재우니의 블로그



이번 내용은 Generic 의 정렬에 대해서 설명을 하고자 합니다. MSDN 에 기재된 제네릭을 한번 읽어보고 진행하죠.

제네릭은 2.0 버전의 C# 언어와 CLR(공용 언어 런타임)에 새로 도입된 기능입니다. 제네릭을 통해 .NET Framework에 형식 매개 변수라는 개념이 처음 소개되었습니다. 형식 매개 변수를 사용하면 클라이언트 코드에서 클래스나 메서드를 선언하고 인스턴스화할 때까지 하나 이상의 형식 지정을 연기하는 클래스와 메서드를 디자인할 수 있습니다. 예를 들어, 다음과 같이 제네릭 형식 매개 변수 T를 사용하면 런타임 캐스트나 boxing 작업에 따른 비용이나 위험을 초래하지 않은 채 다른 클라이언트 코드에서 사용 가능한 단일 클래스를 작성할 수 있습니다. - msdn 발췌

 

제네릭의 정렬을 구현하기 위해서 reflection 기능과 확장 메소드를 이용하여 만들어 볼까 합니다.
C# 3.0 에 새로 도입된 확장메소드라는 새로운 기능이 추가되어 이를 통해 기존 클래스를 변경하지 않고 새로운 메소드를 추가할 수 있습니다. 또한 이미 컴파일된 클래스에까지도 인스턴스 메소드를 추가할 수 있습니다. (http://msdn.microsoft.com/ko-kr/library/bb383977.aspx)

userlist.Sort("Firstname asc, Birthday desc"); 

위의 소스 내용을 보면 Sort 메소드는 확장메소드입니다. 그 파라미터 값으로 SQL 의 정렬 문법과 동일하게
필드명을 먼저 기재하고, 정렬에 대해 ASC 또는 DESC 로 오름차순 또는 내림차순을 정의할 수 있습니다.

이제 Sort 에 대한 확장 메소드를 구현해 볼까요?
sortExpression!s 파라미터값을 가지고 루프를 돌며, 새로운 comparers 를 생성합니다.

// <summary>
/// Sorts the specified list.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">The list to be sorted.</param>
/// <param name="sortExpression">The sort expression; format:
/// @param1 [sortdirection], @param2 [sortdirection], @param3 [sortdirection].
/// Valid sortDirections are: asc, desc, ascending and descending.</param>
public static void Sort<T>(this List<T> list, string sortExpression)
{
    string[] sortExpressions = sortExpression.Split(new string[] { "," },
                StringSplitOptions.RemoveEmptyEntries);

    List<GenericComparer> comparers = new List<GenericComparer>();

    foreach (string sortExpress in sortExpressions)
    {
        string sortProperty = sortExpress.Trim().Split(' ')[0].Trim();
        string sortDirection = sortExpress.Trim().Split(' ')[1].Trim();

        Type type = typeof(T);
        PropertyInfo PropertyInfo = type.GetProperty(sortProperty);
        if (PropertyInfo == null)
        {
            PropertyInfo[] props = type.GetProperties();
            foreach (PropertyInfo info in props)
            {
                if (info.Name.ToString().ToLower() == sortProperty.ToLower())
                {
                    PropertyInfo = info;
                    break;
                }
            }
            if (PropertyInfo == null)
            {
                throw new Exception(String.Format("{0} is not a valid property of type:
                    \"{1}\"", sortProperty, type.Name));
            }
        }

        SortDirection SortDirection = SortDirection.Ascending;
        if (sortDirection.ToLower() == "asc" || sortDirection.ToLower() == "ascending")
        {
            SortDirection = SortDirection.Ascending;
        }
        else if (sortDirection.ToLower() == "desc" ||
                sortDirection.ToLower() == "descending")
        {
            SortDirection = SortDirection.Descending;
        }
        else
        {
            throw new Exception("Valid SortDirections are:
                    asc, ascending, desc and descending");
        }

        comparers.Add(new GenericComparer { SortDirection = SortDirection,
                PropertyInfo = PropertyInfo, comparers = comparers });
    }
    listSort(comparers[0].Compare);
}  


아래는 두개의 요소를 비교하는 GenericComparer 클래스를 구현한 것입니다. 




    public class GenericComparer
    {
        public List<GenericComparer> comparers { get; set; }
        int level = 0;

        public SortDirection SortDirection { get; set; }
        public PropertyInfo PropertyInfo { get; set; }

        public int Compare<T>(T t1, T t2)
        {
            int ret = 0;

            if (level >= comparers.Count)
                return 0;

            object t1Value = comparers[level].PropertyInfo.GetValue(t1, null);
            object t2Value = comparers[level].PropertyInfo.GetValue(t2, null);

            if (t1 == null || t1Value == null)
            {
                if (t2 == null || t2Value == null)
                {
                    ret = 0;
                }
                else
                {
                    ret = -1;
                }
            }
            else
            {
                if (t2 == null || t2Value == null)
                {
                    ret = 1;
                }
                else
                {
                    ret = ((IComparable)t1Value).CompareTo(((IComparable)t2Value));
                }
            }
            if (ret == 0)
            {
                level += 1;
                ret = Compare(t1, t2);
                level -= 1;
            }
            else
            {
                if (comparers[level].SortDirection == SortDirection.Descending)
                {
                    ret *= -1;
                }
            }
            return ret;
        }
    } 




위의 클래스 및 메소드를 이용하여 사용방법을 알아보죠.


public class ExampleUser
{
    public DateTime Birthday { get; set; }
    public string Firstname { get; set; }
}



객체를 담기 위해 필드와 메소드를 구현했습니다.



List<ExampleUser> userlist = new List<ExampleUser>();
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1988, 10, 1), Firstname = "Bryan" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1986, 11, 4), Firstname = "Michael" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1977, 2, 2), Firstname = "Arjan" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1990, 6, 13), Firstname = "Pieter" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1988, 10, 1), Firstname = "Ruben" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1987, 8, 21), Firstname = "Bastiaan" });
        userlist.Add(new ExampleUser
            { Birthday = new DateTime(1987, 8, 21), Firstname = "Pieter" });


 

제네릭 List 를 이용하여 객체를 위와 같이 Add 메소드로 추가했습니다. 


정렬을 하지 않고 할당된 값 그대로 출력하고자 한다면??? Unsorted:



string unsorted = "Unsorted: ";
        foreach (ExampleUser user in userlist)
        {
            unsorted += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }

        




첫번째 이름의 성을 정렬하고자 한다면? Firstname asc



userlist.Sort("Firstname asc");
        string sorted1 = "Sorted by Firstname asc: " + Environment.NewLine;
        foreach (ExampleUser user in userlist)
        {
            sorted1 += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }

        


성과 생일을 둘다 정렬하고자 한다면? Firstname asc, Birthday desc



userlist.Sort("Firstname asc, Birthday desc");
        string sorted2 = "Sorted by Firstname asc, Birtday desc: " + Environment.NewLine;
        foreach (ExampleUser user in userlist)
        {
            sorted2 += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }

        



역반대로 생일을 먼저 asc 순서를 하고  그 다음 "성" 을 asc 문서로 하고자 한다면? Birthday asc, Firstname asc


userlist.Sort("Birthday asc, Firstname asc");
        string sorted3 = "Sorted by Birthday ascending,
                Firstname asc: " + Environment.NewLine;
        foreach (ExampleUser user in userlist)
        {
            sorted3 += String.Format("{0} / {1} {2}", user.Birthday.ToString
                ("dd-MM-yyyy"), user.Firstname, Environment.NewLine);
        }



위의 클래스 및 메소드에 대해서 상세히 설명을 하지 않았는데요. 이는 vs 툴을 가지고 있으시다면 디버깅을 통해서 하나 하나 찾아가시면서 구현한 부분에 대해 이해를 얻으시면 될것 같습니다.


참고로 원문 내용과 동일하게 번역하여 설명을 하지 않았으므로, 원문 사이트 경로에 가셔서 원문 작성자가 기재한

내용을 읽어 보시면 더욱더 도움이 될 것 입니다.




원문 작성자 : Bryan Sumter

원문 사이트 경로 : http://www.codeproject.com/KB/cs/GenericSorter.aspx



감사합니다.



posted by 심재운 (shimpark@gmail.com)


http://cafe.daum.net/aspdotnet/VY7/145