응용프로그램 작성 도중 C#의 소수점 반올림 관련 코드에서 몇몇 자주 사용되는 기능들을 보완하는 코드를 작성하여 올려봅니다. 이 코드는 http://www.latiumsoftware.com/en/delphi/00033.php 의 코드를 바탕으로 작성된 것입니다.

namespace System
{
    // Original Source: http://www.latiumsoftware.com/en/delphi/00033.php
    public static class MathExtension
    {
        public static int Sign(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Math.Sign(x);
        }

        public static int Sign(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Math.Sign(x);
        }

        public static int Integer(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return (int)Math.Truncate(x);
        }

        public static int Integer(
#if CS3
            this
#endif // CS3
            double x)
        {
            return (int)Math.Truncate(x);
        }

        public static decimal Fraction(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return x - Math.Truncate(x);
        }

        public static double Fraction(
#if CS3
            this
#endif // CS3
            double x)
        {
            return x - Math.Truncate(x);
        }

        public static decimal RoundUp(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Integer(x) + Sign(Fraction(x));
        }

        public static double RoundUp(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Integer(x) + Sign(Fraction(x));
        }

        public static decimal RoundDown(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Integer(x);
        }

        public static double RoundDown(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Integer(x);
        }

        public static decimal Round(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Integer(x) + Integer(Fraction(x) * 2m);
        }

        public static double Round(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Integer(x) + Integer(Fraction(x) * 2d);
        }

        public static decimal Fix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            if (x >= 0m || Fraction(x) == 0m)
                return Integer(x);
            else
                return Integer(x) - 1;
        }

        public static double Fix(
#if CS3
            this
#endif // CS3
            double x)
        {
            if (x >= 0d || Fraction(x) == 0d)
                return Integer(x);
            else
                return Integer(x) - 1;
        }

        public static decimal RoundDownFix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Fix(x);
        }

        public static double RoundDownFix(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Fix(x);
        }

        public static int Absolute(
#if CS3
            this
#endif // CS3
            int x)
        {
            return Math.Abs(x);
        }

        public static decimal RoundUpFix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Fix(x) + Absolute(Sign(Fraction(x)));
        }

        public static double RoundUpFix(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Fix(x) + Absolute(Sign(Fraction(x)));
        }

        public static decimal RoundFix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Fix(x + 0.5m);
        }

        public static double RoundFix(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Fix(x + 0.5d);
        }

        public static decimal RoundTo(
#if CS3
            this
#endif // CS3
            decimal x, int d)
        {
            decimal n = (decimal)Math.Pow(10, d);
            x *= n;
            return (Integer(x) + Integer(Fraction(x) * 2m)) / n;
        }

        public static double RoundTo(
#if CS3
            this
#endif // CS3
            double x, int d)
        {
            double n = Math.Pow(10, d);
            x *= n;
            return (Integer(x) + Integer(Fraction(x) * 2d)) / n;
        }
    }
}

사용 예시는 다음과 같습니다.

using System;

namespace ConsoleApplication1
{
    public static class Program
    {
        [STAThread]
        public static void Main(string[] args)
        {
#if CS3
            Console.WriteLine("{0}", (3.3m).RoundUp() == 4m);
            Console.WriteLine("{0}", (-3.3m).RoundUp() == -4m);
            Console.WriteLine("{0}", (3.7m).RoundDown() == 3m);
            Console.WriteLine("{0}", (-3.7m).RoundDown() == -3m);
            Console.WriteLine("{0}", (3.5m).Round() == 4m);
            Console.WriteLine("{0}", (-3.5m).Round() == -4m);
            Console.WriteLine("{0}", (3.1m).Round() == 3m);
            Console.WriteLine("{0}", (-3.1m).Round() == -3m);
            Console.WriteLine("{0}", (3.7m).Integer() == 3);
            Console.WriteLine("{0}", (-3.7m).Integer() == -3);
            Console.WriteLine("{0}", (3.7m).Fix() == 3m);
            Console.WriteLine("{0}", (-3.7m).Fix() == -4m);
            Console.WriteLine("{0}", (3.7m).RoundDownFix() == 3m);
            Console.WriteLine("{0}", (-3.7m).RoundDownFix() == -4m);
            Console.WriteLine("{0}", (3.1m).RoundDownFix() == 3m);
            Console.WriteLine("{0}", (-3.1m).RoundDownFix() == -4m);
            Console.WriteLine("{0}", (3.1m).RoundUpFix() == 4m);
            Console.WriteLine("{0}", (-3.7m).RoundUpFix() == -3m);
            Console.WriteLine("{0}", (3.5m).RoundFix() == 4m);
            Console.WriteLine("{0}", (-3.5m).RoundFix() == -3m);
            Console.WriteLine("{0}", (123.456m).RoundTo(0) == 123.00m);
            Console.WriteLine("{0}", (123.456m).RoundTo(2) == 123.46m);
            Console.WriteLine("{0}", (123456m).RoundTo(-3) == 123000m);

            Console.WriteLine("{0}", (3.3d).RoundUp() == 4d);
            Console.WriteLine("{0}", (-3.3d).RoundUp() == -4d);
            Console.WriteLine("{0}", (3.7d).RoundDown() == 3d);
            Console.WriteLine("{0}", (-3.7d).RoundDown() == -3d);
            Console.WriteLine("{0}", (3.5d).Round() == 4d);
            Console.WriteLine("{0}", (-3.5d).Round() == -4d);
            Console.WriteLine("{0}", (3.1d).Round() == 3d);
            Console.WriteLine("{0}", (-3.1d).Round() == -3d);
            Console.WriteLine("{0}", (3.7d).Integer() == 3);
            Console.WriteLine("{0}", (-3.7d).Integer() == -3);
            Console.WriteLine("{0}", (3.7d).Fix() == 3d);
            Console.WriteLine("{0}", (-3.7d).Fix() == -4d);
            Console.WriteLine("{0}", (3.7d).RoundDownFix() == 3d);
            Console.WriteLine("{0}", (-3.7d).RoundDownFix() == -4d);
            Console.WriteLine("{0}", (3.1d).RoundDownFix() == 3d);
            Console.WriteLine("{0}", (-3.1d).RoundDownFix() == -4d);
            Console.WriteLine("{0}", (3.1d).RoundUpFix() == 4d);
            Console.WriteLine("{0}", (-3.7d).RoundUpFix() == -3d);
            Console.WriteLine("{0}", (3.5d).RoundFix() == 4d);
            Console.WriteLine("{0}", (-3.5d).RoundFix() == -3d);
            Console.WriteLine("{0}", (123.456d).RoundTo(0) == 123.00d);
            Console.WriteLine("{0}", (123.456d).RoundTo(2) == 123.46d);
            Console.WriteLine("{0}", (123456d).RoundTo(-3) == 123000d);
#else // !CS3
            Console.WriteLine("{0}", MathExtension.RoundUp(3.3m) == 4m);
            Console.WriteLine("{0}", MathExtension.RoundUp(-3.3m) == -4m);
            Console.WriteLine("{0}", MathExtension.RoundDown(3.7m) == 3m);
            Console.WriteLine("{0}", MathExtension.RoundDown(-3.7m) == -3m);
            Console.WriteLine("{0}", MathExtension.Round(3.5m) == 4m);
            Console.WriteLine("{0}", MathExtension.Round(-3.5m) == -4m);
            Console.WriteLine("{0}", MathExtension.Round(3.1m) == 3m);
            Console.WriteLine("{0}", MathExtension.Round(-3.1m) == -3m);
            Console.WriteLine("{0}", MathExtension.Integer(3.7m) == 3);
            Console.WriteLine("{0}", MathExtension.Integer(-3.7m) == -3);
            Console.WriteLine("{0}", MathExtension.Fix(3.7m) == 3);
            Console.WriteLine("{0}", MathExtension.Fix(-3.7m) == -4);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.7m) == 3m);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.7m) == -4m);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.1m) == 3m);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.1m) == -4m);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(3.1m) == 4m);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(-3.7m) == -3m);
            Console.WriteLine("{0}", MathExtension.RoundFix(3.5m) == 4m);
            Console.WriteLine("{0}", MathExtension.RoundFix(-3.5m) == -3m);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456m, 0) == 123.00m);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456m, 2) == 123.46m);
            Console.WriteLine("{0}", MathExtension.RoundTo(123456m, -3) == 123000m);

            Console.WriteLine("{0}", MathExtension.RoundUp(3.3d) == 4d);
            Console.WriteLine("{0}", MathExtension.RoundUp(-3.3d) == -4d);
            Console.WriteLine("{0}", MathExtension.RoundDown(3.7d) == 3d);
            Console.WriteLine("{0}", MathExtension.RoundDown(-3.7d) == -3d);
            Console.WriteLine("{0}", MathExtension.Round(3.5d) == 4d);
            Console.WriteLine("{0}", MathExtension.Round(-3.5d) == -4d);
            Console.WriteLine("{0}", MathExtension.Round(3.1d) == 3d);
            Console.WriteLine("{0}", MathExtension.Round(-3.1d) == -3d);
            Console.WriteLine("{0}", MathExtension.Integer(3.7d) == 3);
            Console.WriteLine("{0}", MathExtension.Integer(-3.7d) == -3);
            Console.WriteLine("{0}", MathExtension.Fix(3.7d) == 3);
            Console.WriteLine("{0}", MathExtension.Fix(-3.7d) == -4);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.7d) == 3d);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.7d) == -4d);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.1d) == 3d);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.1d) == -4d);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(3.1d) == 4d);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(-3.7d) == -3d);
            Console.WriteLine("{0}", MathExtension.RoundFix(3.5d) == 4d);
            Console.WriteLine("{0}", MathExtension.RoundFix(-3.5d) == -3d);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456d, 0) == 123.00d);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456d, 2) == 123.46d);
            Console.WriteLine("{0}", MathExtension.RoundTo(123456d, -3) == 123000d);
#endif // CS3
        }
    }
}

위의 코드에서 CS3 라는 매크로를 프로젝트 내에 설정하면 Extension Method로 사용할 수 있으며, 이 매크로의 선언을 해제하면 일반 정적 메서드로 변경하여 사용할 수 있습니다. 여러 메서드들이 있지만 그 중에서도 RoundTo 메서드는 사용 빈도가 매우 많은 함수일 듯 합니다. :-)

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

참가 신청: http://www.devpia.com/Maeul/Contents/Detail.aspx?BoardID=216&MAEULNo=8&no=1137&ref=1137

참가 신청: http://www.devpia.com/Maeul/Contents/Detail.aspx?BoardID=216&MAEULNo=8&no=1137&ref=1137

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

노트: 이 아티클은 Microsoft Visual C# 컴파일러 버전 3.0을 기준으로 작성되었습니다. Mono C# 컴파일러에서는 아직 확인되지 않은 기능입니다.

Extension Method는 대체로 특정 형식과 함께 그 형식을 부모 형식으로 사용하는 다른 형식들에 일괄적으로 영향을 주도록 선언이 설정됩니다. 상속 관계에 있어서는 편리하게 사용할 수 있지만, 비슷한 인터페이스들을 하나 이상 동시에 구현하는 여러 종류의 형식을 선택하는 방법이 잘 보이지 않았습니다.

구현하고자 하는 인터페이스의 수가 늘어나게 되면 일정한 패턴을 형성하게 되는데, 이러한 패턴을 Generic에 대입하고, 이러한 형식 패턴에 대하여 Extension Method를 추가하는 방법을 이번 Article에서 소개하고자 합니다.

다음의 예제 코드를 살펴보기로 합니다.

public static class SpecialExtensions
{
    public static string SpecialMethod<T>(this T target)
        where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        return target.ToString();
    }
}

위의 코드에서 제네릭 형식 T는 where 절의 서술에 따라 다음의 조건을 만족해야 하는 특수성을 가지게 됩니다.

  • struct 키워드가 사용되었기 때문에 값 형식에 속해야 한다.
  • IComparable 인터페이스를 구현하고 있어야 한다.
  • IFormattable 인터페이스를 구현하고 있어야 한다.
  • IConvertible 인터페이스를 구현하고 있어야 한다.
  • IComparable<T> 인터페이스를 구현하고 있어야 한다. 이 때 SpecialMethod<T> 메서드의 인자 T를 그대로 전달하게 된다.
  • IEquatable<T> 인터페이스를 구현하고 있어야 한다. 이 때 SpecialMethod<T> 메서드의 인자 T를 그대로 전달하게 된다.

위의 where 절에서 언급한 이와 같은 인터페이스 구현 패턴은 Primitive Type에 대한 설정으로, 아래와 같이 작동여부가 선택적으로 바뀌게 됩니다. 즉, 구조체이거나 나열 상수라서 모두 SpecialMethod 메서드를 사용할 수 있는 것이 아니고 위의 인터페이스 구현 패턴을 따르는 Primitive Type에 대해서만 SpecialMethod 메서드의 호출이 허용되게 됩니다.

true.SpecialMethod(); // 실패: IFormattable 인터페이스가 구현되지 않음
char.MaxValue.SpecialMethod(); // 실패: IFormattable 인터페이스가 구현되지 않음
Guid.Empty.SpecialMethod(); // 실패: IConvertible 인터페이스가 구현되지 않음
UriFormat.SafeUnescaped.SpecialMethod(); // 실패: IComparable<T>, IEnumerable<T> 인터페이스가 구현되지 않음

byte.MaxValue.SpecialMethod(); // 성공
sbyte.MaxValue.SpecialMethod(); // 성공
short.MaxValue.SpecialMethod(); // 성공
ushort.MaxValue.SpecialMethod(); // 성공
int.MaxValue.SpecialMethod(); // 성공
uint.MaxValue.SpecialMethod(); // 성공
long.MaxValue.SpecialMethod(); // 성공
ulong.MaxValue.SpecialMethod(); // 성공
float.MaxValue.SpecialMethod(); // 성공
double.MaxValue.SpecialMethod(); // 성공
decimal.MaxValue.SpecialMethod(); // 성공

또 다른 방면으로 where 절의 특수 조건 절을 적절히 활용하면, 추상 클래스와 일반 클래스 사이를 선택하여 Extension Method가 달리 선언될 수 있도록 제어하는 것도 가능합니다. 이와 같이 상속 방향 이외의 진행 방향으로 다양하게 Extension Method의 지정 범위를 택하는 기술을 이용하면 좀 더 구체적이고 깊이있는 요구 사항을 처리할 수 있습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

이번 아티클에서 다루고자 하는 C# 3.0의 새로운 사양 역시도 매우 파격적이다. JavaScript 프로그래밍 언어의 장점(?)을 수용한 새로운 사양이다. 바로 Extension Method가 그것이다. 참고로 JavaScript는 prototype이라고 불리우는 예약된 객체에 새로운 메서드를 정의하고 관리할 수 있다.

Extension Method도 파격적인 언어 사양인데, 기존에 존재하지 않았던 제 3자의 메서드를 동적으로 삽입하는 기술이다. 말 그대로 클래스 스펙에 새로운 메서드를 동적으로 추가할 수 있다는 뜻이다. 그렇다면 이것은 객체 지향 프로그래밍 언어 사양을 완전히 무너뜨리는 기술일까? 한 번 살펴보자.!

Extension Method가 사용된 사례를 보고 싶다면 IList<T> 인터페이스와 호환되는 객체 하나를 만들고 이 객체를 IList<T>로 형 변환하여 나타나는 멤버들 중에 Reverse 메서드를 불러보면 된다. 그러나 System.Linq 네임스페이스는 잠시 제거해보기로 하고 컴파일해보자. 결과가 어떨까?

컴파일은 당연히 실패한다. Reverse라는 메서드가 IList<T> 인터페이스와 부모 인터페이스들 모두에 존재하지 않기 때문이다. 하지만 방금전에 제거했던 System.Linq 네임스페이스를 이번에는 넣어보고 컴파일하자. 그러면 신기하게도 컴파일이 잘 이루어진다.

이것이 의미하는 바가 무엇일까? 진정으로 언어 사양을 깨뜨린 것일까? 다행히도 아니다. 만약 이런식으로 메서드를 아무렇게나 추가할 수 있었다고 한다면 참으로 심각한 일이다. 하지만 특정 네임스페이스 안에 정의된 내용을 포함할지 아니할지의 여부에 따라서 확장 기능을 부여할 수 있도록 하는 것이다.

그러면 이제 본격적으로 Extension Method를 정의하는 방법을 살펴보기로 하자.

using System;
using System.Text.RegularExpressions;

namespace OrcasDrive
{
    public static class StringRegexExtender
    {
        public static Regex ToRegex(this string strInstance, RegexOptions nOptions)
        {
            return new Regex(strInstance, nOptions);
        }

        public static Regex ToRegex(this string strInstance)
        {
            return StringRegexExtender.ToRegex(strInstance, RegexOptions.None);
        }

    }

    internal static class Program
    {
        static void Main(string[] args)
        {
            string strExp = "[0-9]+";
            Regex oRegex = strExp.ToRegex();
            Console.WriteLine(oRegex.IsMatch("73820") ? "Number" : "Not Number");
        }
    }
}

굵게 강조 표시한 코드들이 바로 Extension Method를 정의하고 사용하는 방법을 설명하는 부분들이다. 우선 Extension Method를 어떻게 정의했는지 살펴보자.

Extension Method는 우리가 일반적으로 정의하는 정적 함수와 사양이 완전히 같고 단 하나의 인수가 더 필요한 것과 반드시 정적 클래스 안에 정의되어있어야 한다는 두 가지 규칙이 추가된 것이 특징이다.

public static Regex ToRegex(this string strInstance)

this string strInstance라는 파라미터가 눈에 띈다. 이것이 바로 Extension Method를 선언하기 위한 컴파일러에게 통지하는 힌트이다. 여기서 this를 제거한다면 그냥 ToRegex 정적 메서드가 작동하기 위하여 필요한 인수로 작동할 것이다. 하지만 여기에 this 키워드를 추가하여 string 형식, 정확히 말하여 System.String 클래스에 대하여 확장 메서드로 추가할 것을 이야기하고 있는 것이다.

여기서 잠시 생각해 볼 것이 있는데, 이와 같이 Extension Method로 선언하지 않아도 이 함수는 기존의 방법대로 사용할 수 있을까? 답은 "그렇다"이다. 기존의 방법대로 사용하던 Extension Method 선언을 이용하여 편입하던 간에 String 클래스에 대하여 이 ToRegex 메서드가 가지는 입장은 완전히 같은 것이다. 다시 말해, String 클래스 안의 private, protected 멤버와 String 클래스가 정의된 mscorlib 어셈블리 안의 internal 멤버들에 대하여 이 메서드는 접근 권한이 없는 것이다. 역으로 이 ToRegex 메서드를 사용하는 다른 코드의 입장에서도 ToRegex에 대해 상대적으로 움직인다. 겉 모양이 파격적이라고 속까지 그런 것은 아니며 철저히 객체 지향 프로그래밍 언어의 기본을 지켜주고 있는 것이다.

이제 위와 같이 선언한 코드가 사용된 예를 살펴보자.

string strExp = "[0-9]+";
Regex oRegex = strExp.ToRegex();
Console.WriteLIne(oRegex.IsMatch("73820") ? "Number" : "Not Number");

아주 그럴듯하고 멋진 코드가 나왔다.

참고로 this 키워드를 붙일 수 있는 인수는 해당 Extension Method에 하나만 허용된다. 그리고 this 키워드가 붙을 인수는 반드시 함수의 제일 처음에 와야 한다는 강제가 따른다.

자동 프로퍼티는 C# 3.0에서 언어적으로 새롭게 추가된 기능으로 따로 멤버 변수를 선언하고 이것을 캡슐화하는 프로퍼티를 만드는 수고 없이 간단하게 자동화할 수 있는 기능이다. 다음이 그 사용 예이며 매우 간단하다.

public string AutoPropTest
{
  get; set;
}

위와 같이 선언하기만 하면 C# 3.0의 경우 다음과 같은 의미를 가진다.

public static AutoPropTest
{
   get { return this.m_strValue; }
   set { this.m_strValue = value; }
}

그리고 C# 3.0에서 마지막으로 살펴보고자 하는 기능인 익명 배열도 참 간단하다.

var obj = new [] { "test", "test 2", "test 3" };

익명 배열은 익명 형식과 마찬가지로 코드 상에서 형식을 미리 지정하지 않고 컴파일 타임때 형식을 결정하는 기술이다. 위의 코드에서는 string 인스턴스로만 구성된 1차원 배열을 지정하였으므로 실제로는 string[] 형식으로 치환된다. 익명 배열 원소 중에 상속 관계와 관련이 없거나 완전히 다른 형식의 원소가 하나 이상 포함되면 형식을 결정할 수 없다는 오류가 발생하게 된다.

이번 아티클을 기점으로 C# 3.0의 모든 새로운 기능들은 한 번씩 둘러볼 수 있었다. 아티클을 편집하는 현 시점에서 Visual Studio 2008이라는 정식 명칭을 가진 새 버전의 Visual Studio 제품 시리즈가 베타 2 버전으로 배포되고 있으니 언제든지 시험해볼 수 있다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)