이제 앞에서부터 계속 사용해온 쿼리 표현식을 이해해보자.

Customer[] customers = GetCustomers();

//Query expression

var query =

    from c in customers

    where c.Discount > 3

    orderby c.Discount

    select new { c.Name, Perc = c.Discount / 100 };

C# 컴파일러는 쿼리 표현식을 만나면 C# 의 클래스와 인터페이스를 사용하는 표현으로 전환한다.

// C# 표현

var query = customers

   .Where ( c => c.Discount > 3 )

   .OrderBy( c=>c.Discount )

   .Select ( c=> new { c.Name, Perc = c.Discount /100 } );

결국에 customers라는 데이터 소스에 대해서 Where, OrderBy, Select 메소드를 계속 호출하는 것이 된다. Where, OrderBy, Select 메소드는 이미 C# 라이브러리에 정의되어 있다.  그러나 모든 C# 표현이 쿼리 표현으로 바뀔 수 있는 것은 아니다. C#의 어떤 메소드는 쿼리 표현에서 지원해주지 않는다. 그래서 LINQ 표현에서는 두 표현을 혼합해서 사용할 수 있다.

야튼 이처럼 각 쿼리 표현식은 제너릭 메소드를 이용한 표현으로 바뀌는데, 이때 어떤 제네릭 메소드로 변환되어야 하는지 결정하는 과정에 확장 메소드에 적용된 규칙이 그대로 적용된다. 가릿? System.Linq 네임스페이스안의 static 클래스 Enumerable를 보면 이런 메소드들이 정의되어 있는 것을 볼 수 있다.

public static class Enumerable

{

    // Methods

    public static TSource Aggregate<TSource>(this IEnumerable<TSource> source, Expression<Func<TSo

    public static bool All<TSource>(this IEnumerable<TSource> source, Expression<Func<TSource, boo

    public static bool Any<TSource>(this IEnumerable<TSource> source);

    public static IEnumerable AsQueryable(this IEnumerablesource);

    public static double Average<TSource>(this IEnumerable<TSource> source, Expression<Func<TSourc

    public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource item, IEquality

    public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(this IQueryable<TSo

    public static TSource Max<TSource>(this IEnumerable<TSource> source);

    public static TSource Min<TSource>(this IEnumerable<TSource> source);

    public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, E

    public static IEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> sourc

    public static IEnumerable<TSource> ThenByDescending<TSource, TKey>(this IOrderedQueryab

    public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Expression<

    //...

}

정의된 메소드들의 첫번째 인자의 타입이 IQueryable<T>이고 그 앞에 this가 붙어있다. 즉 클래스 Queryable에 정의된 대부분의 메소드는 IEnumerable<T> 인터페이스를 구현한 타입의 객체를 확장하는데 사용된다. IEnumerable<T>를 구현한 객체들에 Where 라는 메소드가 없으면 이곳에 정의된 Where가 호출된다. 분명 Customer[] 타입의 객체 customers에는 Where라는 메소드는 없다. 배열은 IEnumberable<T>를 구현하게 되는데, 따라서 Enumerable에 정의된 IEnumberable<T>의 확장 메소드 Where 메소드를 호출하게 된다( 맞나?  복잡하다. 쓰으... )

만약 메소드를 확장할 수 있는 능력이 없다면 앞의 C# 표현은 다음과 같이 될 것이다.

var query = Select( OrderBy( Where( customers, c=>c.Discount > 3), c=>c.Discount ), c=>new {c.Name, Perc = c.Discount/100} );

뭔소린지 복잡하다. 지금까지 배운 개념들이 쿼리문에서 어떻게 사용되는지를 정리해야 겠다. 

확장 메소드가 있어서 메소드가 호출되는 순서대로 차례로 표현할 수 있다. 그리고 람다 표현식이 있어서 where, orderby같은 키워드에 해당하는 메소드의 로직을 간단히 표현할 수 있다. 또한 익명 타입과 object initializers가 있어서 앞에서 호출한 메소드의 결과를 다음 호출될 메소드의 인자로 넘겨줄 수 있다. 이 모든 것들의 사이 사이에 타입 유추(type inference) 기능이 제 역할을 하고 있다.

무슨 말인지 모르겠더라도 그냥 정리됐다고 넘어가자. 증말 배고파 죽겠다. 헉헉

필자가 현재로서는 이해할 수 없는 것이 있다.

C#2.0부터 Array 타입은 IEnumerable<T>를 구현하기는 한데, 런타임시에 구현이 제공된다고 한다. 다음은 MSDN의 Array 타입 설명에 나와 있는 내용의 일부이다.


".NET Framework 버전 2.0에서 Array 클래스는 System.Collections.Generic..::.IList<(Of <(T>)>), System.Collections.Generic..::.ICollection<(Of <(T>)>) 및 System.Collections.Generic..::.IEnumerable<(Of <(T>)>) 제네릭 인터페이스를 구현합니다. 이 구현은 런타임에 배열에 제공되므로 설명서 빌드 도구에서는 볼 수 없습니다. 따라서 제네릭 인터페이스는 Array 클래스의 선언 구문에 표시되지 않으며, 배열을 제네릭 인터페이스 형식으로 캐스팅(명시적 인터페이스 구현)해야만 액세스할 수 있는 인터페이스 멤버에 대한 참조 항목은 없습니다."


즉 컴파일시에는 IEnumerable<T> 인터페이스를 구현하지 않고 있다는 것이다. 그런데 확장 메소드의 결정은 컴파일 타임에 수행된다. 즉 앞의 Cutomers[]에 대한 확장 메소드 Where를 결정하는 것은 컴파일시에 일어난다는 것이다. 그러나 컴파일시에는 Customers[]가 IEnumerable<T>를 구현하고 있다는 것을 알 수 없다. 음...뭐가 어떻게 된기야. 그렇지만 샘플 코드를 만들어서 돌려보면 돌아간다.

class Program

{

    static void Main(string[] args)

    {

      Customer[] customers = new[] { new Customer("달봉이"), new Customer("봉달이") };

        var query = from c in customers

                    where c.Name == "달봉이"

                    select new { c.Name };

        foreach (var c in query)

        {

            Console.WriteLine(c.Name);

        }

        Console.Read();

    }

}

public class Customer 

{

    public string Name = "";

    public Customer(string name)

    {

        this.Name = name;

    }

}

이게 뭔 시츄에이션인지 이해가 되는 분이 있다면, 연락 좀 오네가이~~배고파서 더 이상 구글링도 못하겠다.

내부적으로는 이런 변환이 수행될지라도 개발자가 쿼리 표현을 이해하기 위해서 모두 이렇게 변환을 수행해보는 것은 번거롭다. 쿼리 표현의 키워드를 이해해서 쿼리 표현식에서 바로 이해하는 것이 효과적, 능률적일게다.

쿼리 표현식은 LINQ to Objects, LINQ to SQL, LINQ to XML 또는 다른 사용자 정의 LINQ 프로바이더중 어떤 것을 사용하든지간에 LINQ 쿼리를 만들때 핵심 표현이다. 쿼리 표현식을 이해하지 못하고서는 안된다는 얘기다.

SQL 쿼리문을 공부할때 from, where, select, orderby 같은 키워드를 먼저 공부했었다. 이제 다음 포스트에서는 이런 쿼리 표현식의 키워드들과 IEnumerable<T> 타입의 확장용 제네릭 메소드들을 알아본다. SQL문과 유사한 것들도 많지만 생소한 것도 많다.

근데. 아...이젠 쪼금 지겨워지려고 한다. 이런 기본적인 키워드들과 메소드에 대한 설명은 잘 설명된 다른 블로그의 포스트에 대한 링크로 대신하고 어쩌면 다음 포스트는 프레임워크관련 주제로 다시 돌아갈 지도 모르겠다. 내 맘이다~~~울랄라.

Posted by dalbong2

타입 유추가 어떻게 일어나는지 그 프로세스에 대한 설명을 하지 않고 지날 수 있기를 바랐는데, 그렇게 되지 못했다. 앞 포스트에서 말한대로 이번 포스트는 타입 유추대한 좀 더 자세한 과정을 알아보도록 한다.

타입 유추가 왜 일어나야 하는가. CLR은 타입 유추를 못하기때문이다. C#이 컴파일하고 나서 코드가 CLR로 넘어가기 전에는 모든 변수, 인자, 파라미터들의 타입이 결정되어 있어야 한다는 것이다. 해서 타입이 지정되지 않은 람다 표현식이 제네릭 메소드의 인자로 넘겨지면 컴파일시 타입 유추가 수행되어야 한다는 것이고 그 유추 과정을 같이 한번 더듬에 보자는 것이 이번 포스트 내용이다. 앞에서 본 코드이다.

public static void Display<T>(T[] names, Func<T, bool> filter)

{

    foreach (T s in names)

    {

        if (filter(s))

        {

            ...       

        }

    }

}

static void Main(string[] args)

{

    string[] names = { "Marco", "Paolo", "Tom", "John" };

    Display(names, s => s.Length > 4);

}

Display<T>(T[], Func<T, bool> filter)를 호출할때, 람다 표현 "s=>s.Length > 4"이 인자로 넘겨지고 있다( 앞에서도 말했지만, 실제로 코드가 인자로 넘어가는 것은 아니다. 이 코드의 포인터 즉 델리게이트 인스턴스가 인자로 넘어간다). 이때 s의 인자는 타입이 지정되지 않고 있다. 이 타입을 유추하기 위해서 타입 유추 프로세스가 일어나는 것이다.

타입 유추가 어떻게 수행되는지에 대한 설명은 다음 MSDN 도움말에 설명되어 있다 : C# Version 3.0 Specification( http://msdn.microsoft.com/en-us/library/ms364047(VS.80).aspx#cs3spec_topic4).

도움말을 읽어봐도 무슨 말인지 잘 모르겠다. 해서 예제를 중심으로 살펴본다. 앞의 예제에서는  T의 타입만 밝혀지면 된다.  T의 타입이 람다식의 인자 s의 타입이 된다.  Display()를 호출할때 사용된 첫번째 인자 names의 타입이 string[]이다. 이것은 Display<T>()의 파라미터 T[]에 해당하고 결국 T는 string이라는 유추에 도달하게 된다. 결국 람다식의 인자 s는 string 타입임을 알 수 있다.

앞의 MSDN 도움말에 나와 있는 좀더 복잡한 예를 보자. 다음은 System.Query.Sequence 클래스에 정의되어 있는 확장 메소드 Select이다.

namespace System.Query

{

   public static class Sequence

   {

      public static IEnumerable<S> Select<T,S>(this IEnumerable<T> source,Func<T,S> selector)

      {

        foreach (T element in source) yield return selector(element);

      }

   }

}

다음은 Name 속성을 갖는 Customer 클래스를 가정하고서는 고객들의 이름을 조회하는데 Select 메소드를 사용하는 코드이다.

List<Customer> customers = GetCustomerList();

IEnumerable<string> names = customers.Select(c => c.Name);

우선 확장 메소드 Select의 호출은 먼저 다음처럼 정적 메소드의 호출로 해석된다.

IEnumerable<string> names = Sequence.Select(customers, c => c.Name);

이제 타입 유추가 시작된다. 먼저 호출하는 코드에서 customers의 타입이 List<Customer>임을 알 수 있고 그래서 제네릭 메소드 Select의 정의로 가서 대응되는 파라미터 IEnumerable<T>의 T는 Customer라는 것이 유추된다. T의 타입이 결정되면 "c=>c.Name"의 c가 유추될 수 있다. 가릿? 어떻게 그럴 수 있냐고?  앞 포스트에서, 델리게이트 타입 Func<T,S>의 정의는 System 네임스페이스에 아래와 같이 정의되어 있다고 했다.

public delegate TResult Func<T, TResult>(T arg)

제너릭의 첫번째 타입 인자 T가 바로 델리게이트가 가리키고 있는 메소드의 인자의 타입이 된다. 즉 앞에서 결정된 T의 타입 Customer가 람다식의 인자 c의 타입이 된다. 그 다음 람다식 c=>c.Name의 반환값이 string이라는 것을 알 수 있고 따라서 Func<T,S>의 S가 string임을 알 수 있다.  제네릭 메소드의 정의를 보면, 델리게이트가 가리키고 있는 메소드의 반환값의 타입이 제네릭 메소드의 두번째 타입 인자와 같다. 즉 "c=>c.Name"의 반환값의 타입이 Func<T,S>의 S의 타입과 동일하다는 것이다. 그리고 Select의 반환값 IEnumerable<S>는 IEnumerable<string>으로 결정되게 된다. 가릿? 오키! 또한  복잡하다. 쓰으...

타입 유추가 진행되는 과정이 조금 복잡한듯해 보이긴 하지만 규칙이 있다.

앞의 링크에 걸린 도움말 페이지를 자세히 보면 알겠지만, 결국 다음과 같은 과정을 따른다.

▶호출하는 메소드쪽의 인자와 메소드를 정의하고 있는 제네릭 메소드쪽의 파라미터는 대응시킨다. 

▶그런 다음 명확히 타입을 밝힐 수 있는 인자의 타입부터 밝혀서 결국은 제네릭 메소드의 타입 인자의 타입도 밝힌다.

▶또는 명확히 밝힌 제네릭 메소드의 타입 인자를 통해서 결국은 호출하는 메소드의 인자의 타입도 밝힌다.

▶이때 System에서 선언되어 있는 제네릭 델리게이트 타입의 정의가 사용될 수 있다.

예를 들어 Func<T1, TResult>의 첫번째 타입 인자 T1이 결정되면, 람다식 c=>c.Name의 인자 c의 타입이 결정될 수 있다든지 또는 람다식 c=>c.Name의 반환값의 타입을 통해서 제네릭 메소드 Func<T1,T2, TResult>의 마지막 타입 인자 TResult의 타입을 유추할 수 있다든지.

결국 앞에서 밝힌 타입을 이용해서 C#은 호출하는 부분을 다시 이렇게 해석하게 된다.

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

반환값은 IEnumerable<string>가 된다.

타입 유추과정이 여엉 개운치가 않다면 앞에서 보여준 링크 페이지를 참고하기 바란다( 면피~~크윽. 룰루랄라~~~).

Posted by dalbong2

객체 지향을 지원하는 언어에서 타입을 확장하는 방법하면 제일 먼저 떠오르는 것은 바로 상속(inheritance)에 의한 메소드의 오버라이딩 또는 오버로딩 또는 하이딩(hinding)이다. 혹시 이 세가지 개념이 구분이 잘 가지 않는다면 구글링을 한번 해 보자. 여튼 타입 확장 하면 상속이라는 것이 제일 먼저 떠오르는 것은 당연하다.

근데 C#3.0부터 새로운 확장 방법을 제공하고 있으니 "메소드를 확장"할 수 있다는 것이다. 즉 사용자 정의 메소드를 마치 원래의 그 타입의 메소드에서 정의한 것처럼 호출해서 사용할 수 있다는 것이다. 클래스에 Sealed로 해서 상속을 허락하지 않는 타입에서도 이런 메소드를 확장하는 방법이 가능하다. C#의 이런 능력은 LINQ문이 좀 더 읽기 쉽고 코딩하기 쉽게 해준다는 것을 알게 될 것이다.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace BulogTestConsole

{

    static class Program

    {

        public static void Display<T>(T[] names, Func<T, bool> filter)

        {

            foreach (T s in names)

            {

                if (filter(s))

                {

                    Console.WriteLine(s);

                }

            }

        }

        static void Main(string[] args)

        {

            string[] names = { "Marco", "Paolo", "Tom", "John" };

            Display(names, s => s.Length > 4);


            Console.Read();

        }

    }

}

public, static으로 된 메소드 Display<>()가 정의되어 있다.  컬렉션과 이 컬렉션의 요소를 필터링할 델리게이트 인자를 받는 메소드이다. 필터링을 통과하는 값을 컨솔에 출력한다.  이 메소드를 호출하는 부분이 Main()에 있다. 첫번째 인자는 문자열 배열이다. 그리고 두번째 인자는 익명 메소드의 델리게이트 인스턴스가 넘어간다. 이 델리케이트 타입을 보면 Func<T, bool> 타입이다. 근데, 이 델리게이트 타입이 정의된 곳이 코드에는 없다. 이 정의는 System 네임스페이스에 정의되어 있다.

public delegate TResult Func<TResult>()

public delegate TResult Func<T, TResult>(T arg)

public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2)

public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3)

public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)

TResult 타입은 반환값의 타입과 동일하고, T1~Tn은 파라미터 타입들과 동일하다. 타입 유추에서 이것은 중요하다. 타입인자 T1~Tn의 타입이 결정되면 파라미터의 타입이 결정되고 TResult 타입이 결정되면 반환값의 타입도 밣혀질 수 있다. 다음 포스트는 타입 유추 프로세스에 대해서 정리를 하려고 한다. 이번 포스트는 메소드 확장에 대한 것이니 여기까지만.

앞의 코드에서 제너릭 메소드 Display를 호출하는 부분은 "이름 배열중에서 길이가 4 이상인 이름을 출력하라"는 표현을 하고 있다. 만약 호출하는 코드를 다음처럼 할 수 있다면 더 직관적일 것이다.

names.Display( s=>s.Length > 4);

  마치 names 즉 string[] 타입에 Display()라는 메소드가 노출되어 있는양. C#3.0의 확장 메소드 기능을 사용하면 이것이 가능하다는 것이다. 이렇게 호출하려면 다음과 같이 메소드의 정의에 하나의 변화만 주면 된다. 다른점을 찾자.

public static void Display<T>(this T[] names, Func<T, bool> filter)

{

...

}

this 키워드를 첫번째 파라미터 names 앞에 붙이고 있다. 이렇게 파라미터 타입 앞에 키워드를 추가하면 그 타입에 마치 현재 정의하고 있는 메소드가 정의되어 있는 것처럼 호출할 수 있다는 것이다. 이것을 "타입의 메소드를 확장한다"고 표현하는 것이다. 만약 그 파라미터 타입이 제네릭 타입이라면 타입 유추를 하고 나서 결정되는 타입의 메소드가 확장된다. 앞에서처럼 T[]앞에 this가 붙어 있으므로 일단 T가 결정되고 난 후의 타입 즉 string[]의 타입에 Display 메소드가 확장되는 것이다.

그러나 타입의 메소드를 확장하는 규칙이 있다. 

확장 메소드는 static 클래스에 정의되어야 한다.

확장 메소드는 static, public 이어야 한다.

this 키워드는 확장될 타입의 파라미터의 타입 앞에 붙어야 한다.

확장 타입은 반드시 첫번째 파라미터 타입이어야 한다.

다음은 decimal 타입에 Double 메소드를 확장하는 샘플 코드이다.

static class ExtensionMethods

{

    public static decimal Double( this decimal d )

    {

        return d+d;

    }

}

decimal d = decimal.Double( 4)와 같은 호출이 가능하다는 것이다.

names.Display()로 호출하니 이 Display 메소드가 어디에 정의되어 있는지 그 메소드를 검색하는 절차가 필요하다. 현재 네임스페이스와 using문을 사용해서 포함시킨 모든 네임스페이스에 있는 모든 static 클래스에서 static, public Display 메소드를 검색한다. 만약 두개 이상의 타입에서 동일한 확장 메소드를 가지고 있다면 컴파일러는 에러를 발생시킨다.

만약 인스턴스 메소드가 이미 정의되어 있는 경우 즉 동일한 이름과 시그너쳐를 갖는 메소드가 이미 타입에 정의되어 있다면, 인스턴스 메소드를 먼저 호출할까 아니면 확장 메소드를 먼저 호출할까? 인스턴스 메소드 승! 동일한 확장 메소드와 가상 메소드가 같은 타입에 정의되어 있다면 ? 가상 메소드가 승!

다음은 호출한 메소드를 검색하는 로직이다. 가상 메소드를 결정하는 기존의 로직은 동일한다.

▶먼저 현재 호출하는 객체의 타입을 확인하고 그 타입의 정의로 이동한다.

▶그곳에 호출하는 메소드가 있는지 확인한다.

▶해당 메소드가 인스턴스 메소드인지, 가상 메소드인지 확인한다( abstract, virtual, override가 붙어있으면 가상 메소드가 된다).

▶인스턴스 메소드라면 해당 메소드를 바로 호출한다.

▶가상 메소드라면 현재 객체의 실제 타입(concrete type)이 뭔지를 확인한다.

▶실제 타입의 정의도 다시 이동한다.

▶실제 타입에서 실제 구현하고 있는 해당 메소드를 호출한다.

가상 메소드와 인스턴스 메소드가 없다면 그제서야 확장 메소드를 검색한다.

추가

확장 메소드를 검색할때도 현재 호출하는 객체의 타입에 확장 메소드가 없다면 부모 타입에 대해서 확장 메소드가 정의되었는지를 확인한다. 가릿? 다음처럼 Object 타입에 대해서 Display라는 메소드가 확장되어 있다고 하자.

static class Displayer

{

    public static void Display( this object o)

    {

        string s = o.ToString();

        Console.WriteLine( s );

    }

}

이런 상황에서 다음처럼 사용자 정의 타입 Customer의 객체에 대해서 Display 메소드를 호출했다고 하자.

Customer c = new Customer();

c.Name = "달봉이";

c.Display();

물론 Customer 타입에는 Display 메소드가 없다고 하자. 그럼 Cutomer의 부모 타입인 object에 확장되어 있는 Display 메소드가 호출된다는 것이다. 만약 동일한 메소드가 Customer에 대해서 확장되어 있다면?

static class Displayer

{

    public static void Display( this object o)

    {

        string s = o.ToString();

        Console.WriteLine( s );

    }

    public static void Display( this Customer c)

    {

        string s= String.Format( "Name={0}", c.Name );

        Console.WriteLine( s );

    }

}

이런 경우는 Customer 타입의 확장 메소드를 사용하게 된다.

사실 필자도 확장 메소드를 아직 사용해보지는 않았다. 필자도 처음에는 가상 메소드의 호출하는 메커니즘과 비슷해서 어떻게 받아들여야 할지 몰랐다. 근데 차이가 있었다. 확장 메소드의 결정은 "컴파일타임"에 일어나고 가상 메소드의 결정은 "런타임"에 수행된다는 것이다.

즉 어떤 객체에 대해서 메소드를 호출했을때 이 메소드가 확장 메소드인지 여부는 컴파일타임에 결정된다. c.Display()를 호출했을때 Customer 타입에는 Display가 정의되어 있지 않다는 것을 알고 확장 메소드인지 여부를 확인하는 절차를 따라가게 된다. 그래서 사용하고 있는 네임스페이스에 있는 모든 static 클래스에 정의된 public, static 메소드들을 확인하게 된다. 그래서 Customer 또는 그 베이스 클래스에 Display가 정의되어 있는지를 확인한다. 그래서 Customer 타입에 대해서 확장된 Display 메소드가 확장되었다. 여기까지가 컴파일 타임시에 일어난다.

이제 런타임시 c.Display() 메소드를 호출하는 코드에 다다르면 컴파일 타임에 결정된 그 Display()가 호출된다. 이 메소드는 public static이다.  static 메소드는 가상 메소드일수가 없다. 즉 public static인 Display의 가상 메소드 버전은 있을 수 없다는 것이다. 런타임시에도 그대로 컴파일 타임에 결정된 그 메소드가 호출될 수 있다.

마아...여기까진데. 쩜 복잡한가 싶다.

근데 문서를 보면 확장 메소드를 배울수록, 강력한 타입의 특성을 유지하면서도 언어가 유연해질 수 있다는 것을 알게 된단다. 가상 메소드의 강력함을 안다면 타입의 확장 메소드의 위력도 어느 정도는 이해할 수 있겠다 싶지 않은가. 나만 그런가. 야튼 좋텐다.  다음 포스트에서는 타입 유추에 대해서 좀 더 정리한다.

Posted by dalbong2

람다 표현식. 참 이름도 신기하다. 이 녀석을 뭔지 미리 말하면 이렇다. 델리게이트가 사용될 자리에서 매우 심플한 표현으로 대신할 수 있는 녀석이다. 

Customer[] customers = GetCustomers();

//LINQ 쿼리문

var query =

    from c in customers

    where c.Discount > 3

    orderby c.Discount

    select new { c.Name, Perc = c.Discount / 100 };

// C# 표현

var query = customers

   .Where ( c => c.Discount > 3 )

   .OrderBy( c=>c.Discount )

   .Select ( c=> new { c.Name, Perc = c.Discount /100 } );

첫번째와 같은 LINQ 쿼리문을 만나면 C#은 두번째 표현으로 해석하게 된다. LINQ쿼리문의 "c.Discount > 3"이 두번째의 "c=>c.Discount > 3"에 해당한다. "=>"이 들어간 표현을 바로 람다 표현식이라고 한다. 이 람다 표현식은 익명 메소드가 더 간단해진 표현이다. 이 표현식을 함 풀어서 말하면 이렇다. "파라미터 c를 받아서 그 c의 속성 Discount값이 3보다 큰지를 확인해서 그 불린값을 반환한다"이다. 이런 의미를 표현하기 위해서 어떻게 표현이 진화하게 되었는지 이제 제대로 알아보자.

다음은 이어질 설명에서 계속 사용될 메소드 Aggregate에 대한 정의이다.

public delegate T Func<T>(T a, T b);

public class AggDelegate

{

    public List<int> Values;

    public T Aggregate<T>(List<T> l, Func<T> f)

    {

        T result = default(T);

        bool firstLoop = true;

        foreach (T value in l)

        {

            if (firstLoop)

            {

                result = value;

                firstLoop = false;

            }

            else

            {

                result = f(result, value);

            }

        }

    }

}

제네릭 메소드 Aggregate()에는 두개의 파라미터가 정의되어 있다. 첫번째 인자는 컬렉션을 받아들이고 두번째 인자는 델리게이트 Func<T> 타입의 인자 f를 받아들인다. Func<T> 정의를 보면 T 타입의 인자를 두개 받아들이는 메소드를 가리키는 포인터를 받아들인다는 것을 알 수 있다. Aggregate의 본문에 있는 forech문 내부에서는 데이터 소스를 순환하면서 각 요소값에 대해서 f를 호출하고 또 다시 그 결과값과 다음 요소의 값을 델리게이트 f에 넘겨서 연산을 반복적으로 수행한다.

여기서 예제로 정의한 Aggregate() 메소드이기는 하지만, 이렇게 컬렉션을 데이터 소스로 하고 그 데이터 소스에 대해서 루프를 돌면서 외부에서 정의한 연산 로직을 수행하는 형태에 대한 구조를 기억하고 있을 필요가 있다. 외부 연산 로직이 어떻게 구현될지는 Aggregate 메소드 내부에서는 모른다. 외부의 코드에서 즉 개발자가 원하는 대로 정의하면 된다. 다음은 Aggregate()를 호출하때 그 연산 로직을 제공하는 샘플 코드이다.

 public static void Demo()

{

    AggDelegate l = new AggDelegate();

    int sum;

    sum = l.Aggregate(l.Values, delegate(int a, int b) { return a + b; });

}

Aggreate()의 첫번째 인자 즉 데이터 소스로 l.Values가 전달되고 있는데, 이것은 List<int> 타입의 컬렉션이이다. 반드시 이 컬렉션의 위치가 AggDelegate에 멤버로 정의될 필요는 없다. 데이터 소스로서 컬렉션이 주어졌다는 것이 중요하다. 그리고 Aggregate()를 호출할때 두번째 파라미터를 보면 delegate 키워드를 통해서 익명 메소드를 하나 정의하고 있는데, 이것이 바로 외부에서 제공하는 연산로직이다.

이것을 말하기 전에 우선 타입 유추에 대해서 알아보자. 앞에서 정의한 Aggregate() 메소드는 제네릭 타입 <T>를 갖는 제네릭 메소드이지만 호출하는 코드에서는 <T>가 없다. 즉 T가  어떤 타입으로 유추되었는지 알아보자는 것이다. 인자로 주어진 l객체의 Values 값이 첫번째 파라미터 타입 List<T> 에 해당하는 객체이다. l.Values는 AggDelegate 타입에 정의되어 있는  List<int> 타입이다. 즉  List<T>는 List<int>라는 것을 알 수 있고 결국 T는 int 라는 것을 알 수 있다. 또는 두 번째 인자 익명 메소드의 파라미터( int a, int b)를 통해서도 타입 T를 int임을 유추할 수 있겠다. 타입 유추를 하는 프로세스가 MSDN에 나와있기는 한데 사실 아직 필자도 다 외지 못하고 있다. 다음에 기회가 되면 정리해 보도록 하겠다. 물론 생각나면 -_-;;

여튼 이제 타입 T가 결정되었다. 두번째 인자를 알아보자. int 타입의 두 인자를 받아들여서 그 합을 반환하는 로직을 구현하고 있다. 이제 이 표현이 좀 더 간단한 모습으로 진화한 결과를 보겠다.

sum = l.Aggregate(l.Values, (int a, int b) => { return a + b; });

파라미터 목록 앞에 있는 delegate 키워드를 없앴다. 대신에 "=>" 표시를 파라미터 목록과 메소드 본문 사이에 두었다. 이제 람다 표현식이 드러나기 시작한다.  "int 형 인자 a,b를 받아들여서, a와 b의 합인 a+b를 반환한다"로 읽을 수 있다.  머릿속에 항상 염두에 두고 있어야 할 것은 Aggregate 메소드의 두번째 인자로 코드가 넘어가는 것이 아니다. 그 코드를 가리키고 있는 포인터 즉 델리게이트 인스턴스가 넘어간다는 기억하고 있어야 한다. 이 표현이 컴파일되면 delegate를 사용한 익명 메소드와 동일한 결과가 된다. 이 람다 표현이 진화의 끝은 아니다. 좀 더 간단한 표현으로 될 수 있다.

sum = l.Aggregate(l.Values, (a, b) => { return a + b; });

익명 메소드의 파라미터의 타입이 생략되었다. 그렇다고 T를 모르는 것은 아니다. 앞에서 본 것처럼 l.Values 첫번째 인자를 추적해가다보면 T의 타입이 결정된다. 이 표현이 끝은 아니다. 다시 진화할 수 있다. 만약 {}블럭내에 return 문 하나만 있다면 return과 {}블럭도 생략될 수 있다.

sum = l.Aggregate(l.Values, (a, b) => a + b );

람다 표현식의 파라미터가 하나뿐이라면 또 표현이 간단해 질 수 있다( 이 예제에서 AggregateSingle()의 두번째 파라미터는 FuncSingle<T>(T)와 유사한 델리게이트 타입의 인스턴스가 될 것이다).

int sum = 0;

sum = AggregateSingle(l.Values, x=>sum+=x );

인자 목록을 감싸고 있는 괄호가 없어졌다.  간단해졌다. 이전에도 자주 봐 왔던 LINQ의 C# 표현이 이제 되어 가고 있다. 그러나 람다 표현에서 인자가 없게 되면 다시 => 앞에 괄호가 나타난다. 다음의 마지막 표현이다. 람다 표현식의 진화 과정을 정리하면 다음과 같다.

delegate(int a, int b) { return a + b; }

(int a, int b)=>{ return a + b; }

( a,  b)=>{ return a + b; }

( a,  b)=>a+b;

(x) => sum += x;

x => sum+=x;

()=>sum +1;

람다식을 사용하면 표현이 아주 간단해질 수 있다.  C#3.0부터서는 LINQ 표현을 읽기 쉽고, 간단하게 해주는 기능이 또 하나 있는데, 기존 타입의 메소드를 확장할 수 있는 방법을 제공한다는 것이다. 이미 클래스 타입을 정의했고, 그 소스 코드에는 접근할 수가 없다. 예를 들어 String 타입에 사용자 정의 Display()같은 메소드를 추가할 수 있다는 것이다. JavaScript의 property 속성을 떠올리는 사람이 있다면 바람직한 연상을 하고 있는 것이다. 그와 비슷한 기능을 C#에서 제공하고 있다는 것이다. 이것은 다음 포스트에서.

Posted by dalbong2

제네릭이 뭔지 알아본다. LINQ 표현에 제네릭이 직접 표현되지 않더라도, C# 표현으로 변경하면 보이지 않던 제네릭 표현이 나타나게 된다. C# 표현의 쿼리를 이해할 수 있어야 LINQ 표현을 정확히 이해할 수 있는 바, 이 녀석을 모르고서는 LINQ 표현을 제대로 이해할 수 없다는 얘기가 되겠다.

다음과 같은 상황을 생각해보자. 메소드나 클래스를 정의할때 그 구성 요소에 대한 타입을 미리 알 수 없을때, 즉 여러 타입을 지원하고 싶을때 어떻게 해야 하나. object 타입을 사용하면 될 거라고 생각하고 있나. C# 1.X까지는 정답이다. 다음은 IList 타입의 컬렉션의 Add() 메소드에 대한 정의이다.

public interface IList : ICollection, IEnumerable

{

    int Add(Object value);

}

IList 인터페이스에는 Add()라는 메소드가 정의되어 있는데, 그 파라미터의 타입으로 object 타입을 사용하고 있다. 그럼 어떤 타입의 객체도 모두 포함시킬 수 있게 된다. 그러나 단점은 컬렉션에서 요소 하나를 가져올때, 컬렉션에 저장될때의 타입으로 변환이 이뤄져야 한다는 불편함이 있다.

String s = "test";

IList list = new ArrayList();

list.Add( s );

//...

String ss = (String)list[0];

그러나 단순히 불편함의 차원을 넘어서 안전하지 못한 면이 있다. 컴파일러가 그 타입 변환을 체크하지 않기 때문에 런타임에서야 타입 변환의 에러가 발생할 수 있다는 것이다. 또한 런타임시에 박싱, 언박싱이라는 단계를 거치기 때문에 성능상의 문제도 있을 수 있다.

C#2.0부터는 더 좋은 정답이 나왔다.  제네릭 !

public interface IList<T> : ICollection<T>,IEnumerable<T>, IEnumerable

{

    void Add(T item );

}

제네릭 타입의 IList에 대한 예이다. T 대신에 실제의 어떤 타입을 사용해도 된다. 제네릭을 사용한 코드는 좀 더 명확하고 안전하다. 명확하다는 말은 IList가 어떤 타입의 객체를 포함하는 컬렉션인가를 미리 알 수 있다는 것이고, 미리 알 수 있다는 것은 컴파일러가 추가되는 요소의 타입을 미리 체크할 수 있고 그리고 조회된 요소가 어떤 타입의 변수에 할당될 수 있는지를 미리 알 수 있다는 것이다. 그래서  컴파일 타임에 타입에 대한 체크를 할 수 있어서 좀 더 안전하다는 것이다. 

다음 그림은 Vistual Studio.NET 2008에서의 코딩하는 모습을 캡쳐하고 있다.

1092253364

String 타입의 항목을 받는 List 객체라는 것을 코딩시에도 알고 있기 때문에, Add() 메소드를 호출하면 그림처럼 "string item"처럼 String 타입의 항목을 넣으라는 인텔리센스 표시도 가능해진다.

성능 또한 박싱, 언박싱이 일어나지 않는다.

List<string> sList = new List<string>();

sList.Add("달봉이");


List<int> iList = new List<int>();

iList.Add(0);

이 코드에 해당하는 IL코드를 런타임시, JIT 컴파일러가 머신 언어로 다시 변경할때  String용 List와 int용 List 타입을 각각 만들어 내 버린다. 즉 런타임시에는 파라미터 또는 반환값 또는 코드내의 타입이 결정되어 버린다. 따라서 런타임시에 박싱/언박싱이 일어나지 않게 되는 것이다.

IList<T>라는 타입을 보면 <T>라는 표시가 있는데, "T타입의, Of T type"으로 읽으면 된다. 즉 "IList<String> ss"은 "String 타입의 IList 인스턴스 ss"라고 읽는다. 다르게 읽어도 상관은 엄따. T를 타입 매개변수(type parameter)라고 하는데, 여기서 반드시 문자열로 "T"를 사용할 필요는 없다. 다른 문자, 또는 문자열을 사용해도 된다.  그리고 <>안에 여러개의 타입 매개 변수를 사용할 수 있다. 다음과 같은 제네릭 메소드가 있을 수 있다.

T Method<T, A0>(A0 a, A0 b)

{

    //....

    return r;

}

두 개의 타입 파라미터를 이용하고 있다.  메소드 파라미터 a,b는 A0 타입이고 가공한 후의 리턴값 r은 타입 T임을 표현하고 있다. 

제네릭(generics), 제너릭 타입, 제네릭 메소드 등과 같은 표현을 보면 <T>와 같은 표현이 들어가 있는 타입, 함수 등을 생각하면 된다. 이때 T를 타입 매개변수(type parameter)라고 한다.

이 제너릭 표현에 편안해질수록 LINQ 공부가 그만큼 더 쉬워질 것으로 보인다. 이 포스트에서 제너릭에 대한 모든 문법적인 표현을 설명하지는 않는다. 제네릭에 대한 좀 더 자세한 문법적인 표현에 대해서는 다음 MSDN 도움말을 참고하기 바란다.

제네릭(C# 프로그래밍 가이드)

제네릭 메소드를 호출할때 메소드에서 이용하고 있는 타입을 모두 컴파일러에게 알려줘야 한다. 그러나 type inference( 타입 유추? 정도로 번역할 수 있겠다) 메커니즘을 사용하면 개발자가 직접 타입 파라미터에 해당하는 실제 타입을 알려주지 않아도 컴파일러가 타입을 유추할 수 있다. 이런 컴파일러의 기능을 이용하면 제너릭 메소드를 호출하는 표현이 간단해 질 수 있다. 타입 유추 기능 또한 LINQ 표현을 이해하는데 아주 중요한 개념이라고 할 수 있다. 제너릭 메소드 호출 및 타입 유추에 대해서는 다음 포스트에.

Posted by dalbong2

앞의 포스트에서 다음 코드를 보았다.

delegate void SimpleDelegate();

public class Writer

{

    public string Text;

    public int Counter;

    public void Dump()

    {

        Console.WriteLine(Text);

        Counter++;

    }

}


public class DemoDelegate

{

    void Repeat10Times(SimpleDelegate somework)

    {

        for (int i = 0; i < 10; i++)

            somework();

    }

    void Run1()

    {

        Writer writer = new Writer();

        writer.Text = "C# demo";

        this.Repeat10Times(writer.Dump);

        Console.WriteLine(writer.Counter);

    }

}

이 코드에서는 SimpleDelegate에 의해서 실행될 코드와 인자 역할을 하는 데이터를 가지고 있는 Writer 클래스가 정의되어 있다. 이것을 없애고 다음과 같은 표현으로 변경될 수 있다.

delegate void SimpleDelegate();

public class DemoDelegate

{

    void Repeat10Times(SimpleDelegate somework)

    {

        for (int i = 0; i < 10; i++)

            somework();

    }

    void Run1()

    {

        int counter = 0;

        this.Repeat10Times(

            delegate

        {

           Console.WriteLine("C# demo");

           counter++;

        }

        );

        Console.WriteLine(writer.Counter);

    }

}

Repeat10Times()의 인자로서 메소드명이 들어갈 자리에  메소드를 정의하고 있는 코드가 들어가 있다. 그리고 데이터 멤버 counter는 Repeat10Times()의 래퍼 메소드에 포함되어 있다.  C# 컴파일러는 delegate 키워드 이하의 블럭{...}사이의 코드를 이용해서 내부적으로 Writer와 유사한 클래스와 메소드를 만들어낸다. 그리고 그 인스턴스를 만들고 그것의 메소드에 대한 포인터를 Repeat10Times()의 인자로 넘긴다. Repeat10Times()입장에서는 넘어오는 인자의 메소드명이나 클래스명이 중요한 것이 아니다. SimpleDelegate 델리게이트 타입이 정의하고 있는 시그너쳐와 동일한 메소드 포인터가 넘어오는가가 중요하다. 그리고 실행될 메소드의 코드에 대한 포인터라는 것이 중요하다. 이 말은 앞의 코드의 모양이 Repeat10Time()의 인자로 코드가 넘어가는 듯한 모양이지만, 실제 내부적으로는 그 코드가 정의되어 있는 곳의 포인터가 넘어간다는 것이다. 그 포인터의 이름은 어떻든 상관없다.

delegate 키워드와 블럭 {}에 의해서 정의된 메소드를 이름이 없는 메소드 즉 익명 메소드(anonymous method)라고 한다.

인자가 필요한 익명 메소드를 정의해보면 다음과 같다.

delegate void TowParamsDelegate(string text, int age);

public class DemoDelegate

{

    void Repeat10Times(TowParamsDelegate somework)

    {

        for (int i = 0; i < 10; i++)

            somework("C# demo", i);

    }

    void Run1()

    {

        this.Repeat10Times(

            delegate( string text, int age )

        {

            Console.WriteLine("{0}, {1}", text, age);

        }

        );

    }

}

모양이 아직까지는 좀 우습게 보이지만 다음에 멋지게 변한다. 멋지다는 것은 아주 직관적으로 변하게 된다는 것이다. 즉 코드만 보면 무슨 말인지 바로 알 수 있는 모습으로 말이다.

다시 한번 더 LINQ 쿼리 표현을 보자.

var query =

        from c in customers

        where c.Discount > 3

        orderby c.Discount

        select new { c.Name, Perc = c.Discount / 100 };

다음은 동일한 C# 표현이다.

var query = customers

                   .Where(c => c.Discount > 3)

                   .OrderBy(c => c.Discount)

                   .Select(c => new { c.Name, Perc = c.Discount / 100 });


Where() 메소드의 인자로 넘어가고 있는 "c=>c.Disount>3" 부분이 바로 익명 메소드 표현이 진화해서 된 것이다. 인자와 메소드의 바디 부분이 이곳에 모두 표시되어 있다. 단지 메소드의 이름이 없을 뿐이다.
그러나 아직 이 표현을 모두 이해할 준비는 되지 않았다. 이제 다음 포스트에서 제너릭, Type inference( 타입 추론?), 람다 표현(lamda expression)에 대해서 설명한다. 그러고도 몇 가지 더 배워야만 이 표현을 이해할 수 있게 될 것이다.

설명을 이렇게 하고 싶지 않았는데! 머릿속에 그림을 그려가면서 이해할 수 있도록 하고 싶었는데. 시간이 없고 마음이 급하다 보니 또 이렇게 형식적으로 흘러간다. 써글!

Posted by dalbong2

앞에서 얘기한대로 이제 C#언어에 대해서 알아보겠다. LINQ 쿼리문을 이해하기 위해서 필요한 C#언어 요소들을 하나씩 알아가보겠다. 먼저 델리게이트 ! 델리게이트하면 필자에게는 다음과 같은 내용이 떠오른다.

▶클래스와 같은 일종의 타입이다. 다음과 같은 방법으로 델리게이트 타입을 정의한다.

delegate void TwoParamsDelegate(string name, int age);

▶이것도 타입이니 인스턴스를 만들 수 있다.

TwoParamsDelegate c = new TwoParamsDelegate(인자);

▶델리게이트 타입의 객체에는 다른 객체의 메소드에 대한 포인터가 할당된다.

앞의 TwoParamsDelegate 델리게이트 타입은 파라미터로 string, int 타입의 값을 받고 void를 반환하는 메소드를 할당받을 수 있다.

public Class1()

{

    DemoDelegate d = new DemoDelegate();

 

    TwoParamsDelegate c = new TwoParamsDelegate(d.MethodC);

}

이때 DemoDelegate 객체의 수명은 TowParamsDelegate 인스턴스의 객체만큼 길어진다. 델리게이트 인스턴스가 장수하면 그 델리게이트가 참조하고 있는 객체도 그 만큼 장수하게 된다.

이런 이유로 해서 정적(static) 멤버인 델리게이트에 메소드를 할당할때는 주의해야 한다는 것이다. 자세한 것은 "이벤트 핸들러에 의한 메모리 증가(http://www.dalbong2.net/182)" 포스트를 참조하자. 이벤트 멤버도 델리게이트 타입이라는 것을 알고 있을 것이다. 모르면 지금 알면 된다. -_-;; 이 포스트에서는 웹폼/윈폼에서 static 델리게이트를 잘못 사용하면 페이지가 한번 로딩한 다음 계속해서 메모리에서 내려가지 않을 수 있다는 것을 말하고 있다.

▶중요한 것은 그 할당된 포인터를 통해서 해당 메소드를 호출할 수 있다는 것이다.

c("달봉이", 20 ); //^^

LINQ를 공부하기 위해서는 여기까지가 중요하다.

델리게이트를 좀 더 생각해보면 델리케이트에는 메소드 포인터를 하나만 할당할 수 있는 것이 아니라 여러 개의 포인터를 추가할 수 있다. 따라서 포인터를 할당한다기 보다는 정확히는 포인터를 추가한다는 표현이 옳을 것이다. 이벤트 멤버도 특수한 델리게이트 객체인데, 이벤트에 여러 개의 핸들러를 "+=" 연산자를 이용해서 추가한 것을 생각하면 된다.
또 하나가 있는데….뭐였드라….에이 까먹었다.

앞에서는 델리게이트 인스턴스를 생성할때 new 키워드를 사용했다.

앞의 포스트의 LINQ 표현을 다시 보자. 그러나 메소드 명만을 지정할 수도 있다.

public Class1()

{

    DemoDelegate d = new DemoDelegate();

    TwoParamsDelegate c = d.MethodC;

}

C#컴파일러는 코드를 통해서 델리게이트 타입을 추론한다. 그래서 new TwoParamsDelegate 코드를 만들어낸다.  그래서 IL코드가 될때는 new를 사용할때와 동일한 코드가 된다.

IL코드가 뭔지는....? .NET 언어로 작성된 코드는 컴파일을 하면 일단 중간 단계의 IL 코드로 변한다. Visual Basic으로 작성하든 C#으로 작성하든 모두 IL코드로 컴파일되는데, IL단계의 코드는 실제로 실행될때 JIT컴파일러에 의해서 다시 머신코드로 변한다.   왜 이런 단계를 두었냐고 물으신다면..오늘은 머리가 딸린다.   그냥 넘어가자.         

델리게이트는 흔히 기존의 코드에 어떤 코드를 끼워 넣는 경우 자주 사용된다.

delegate void SimpleDelegate();

public class Writer

{

    public string Text;

    public int Counter;

    public void Dump()

    {

        Console.WriteLine(Text);

        Counter++;

    }

}

 

public class DemoDelegate

{

    void Repeat10Times(SimpleDelegate somework)

    {

        for (int i = 0; i < 10; i++)

            somework();

    }

    void Run1()

    {

        Writer writer = new Writer();

        writer.Text = "C# demo";

        this.Repeat10Times(writer.Dump);

        Console.WriteLine(writer.Counter);

    }

}

SimpleDelegate라는 델리게이트 타입을 하나 정의하고있다. 그리고 DemoDelegate 클래스의 Repeat10Times() 메소드의 파라미터로 받아들여서 해당 델리게이트 인스턴스가 가리키는 메소드를 10번 호출하고 있다. SimpleDelegate 델리게이트 타입이 정의하고 있는 시그너쳐에 맞는 메소드는 어떤 메소드든지 Repeate10Times()의 인자로 넘겨져서 10번 호출되는 것이다. 그러나 앞의 Writer 클래스를 없애고 좀 더 간단한 표현으로 변경될 수 있다. 다음 포스트의 주제이다.

델리게이트가 LINQ 쿼리문의 어디에서 사용되는지 미리 함 보자.

var query =

                from c in customers

                where c.Discount > 3

                orderby c.Discount

                select new { c.Name, Perc = c.Discount/100 };

이처럼 SQL 과 비슷한 표현을 쿼리 표현(query expression)이라고 한다. 이 표현은 컴파일러에 의해서 C#언어가 이해할 수 있는 다음과 같은 표현으로 변한다. 만약 Visual Basic을 사용하면 다른 표현으로 변경될 것이다.

var query = customers

                   .Where ( c => c.Discount > 3 )

                   .OrderBy( c=>c.Discount )

                   .Select ( c=> new { c.Name, Perc = c.Discount /100 } );

이 표현중에서 customers.Where(c => c.Discount > 3)은 익히 알고 있는대로, customers라는 객체에 Where()라는 메소드가 호출되는 것이고 인자로 "c=>c.Discount>3"이라는 인자가 넘어가는 평범한 문장이다. 그리고 그 반환된 결과에 OrderBy() 메소드가 호출되는 것이고...이렇게 계속된다.

이곳에서 "=>"가 포함된 표현 "c=>c.Discount > 3"은 델리게이트가 진화해서 된 부분임을 알게 된다. 델리게이트가 어떻게 진화할 지 아직 얼른 감이 오지 않을 것이다. 그럼 다음 포스트에서 보자.

Posted by dalbong2

Application Block같은 .NET용 개발 프레임웤을 보면 이제 제너릭 정도는 자연스럽게 사용되고 있다. 프레임웤에 대한 공부를 더 진행하기 전에 이쯤해서 C#에 대한 정리를 해야 할 것 같다. 그러나 하는 김에 제너릭뿐만 아니라 C#3.0이상에서 소개하고 있는 표현들에 대한 정리도 해 볼려고 한다. 그러나 필자도 아직 다 공부를 못한 부분이기에 시간이 좀 걸려서 진행될 것 같은 분위기다. 이번 포스트부터도 연재 형태로 갈려고 한다. 최종 목표는 LINQ라는 것의 이해이다. 해서 연재 제목을 이름하여 "LINQ 시리즈"로 정했다.

들어가기 전에 먼저 하나 생각해 보도록 하자. 고객이 여러명 있다. 이 고객들에 대한 정보가 어떤 형태로 있을 수 있을까? 우선 Customer라는 사용자 정의 타입이 있어서 이 타입의 인스턴스들이 컬렉션 구조로 메모리에 있을 수도 있다.  그래서 for문 또는 foreach문으로 각 인스턴스에 접근할 수 있다. 익숙한 방식이다. 그리고  SQL에 저장되어 있을 수도 있고 XML에 저장되어 있을 수도 있다.

그렇다면 고객의 정보가 어디에 있든 상관없이 한방(!)의 쿼리로 필요한 고객에 대한 정보를 검색해서 가져올 수 있는 방법을 찾는다면 발칙한 발상일까? 한방의 쿼리라 함은 개발자는 같은 표현을 사용하더라면 컴파일러든 .NET 프레임웤든 밑단의 인프라가 알아서 해석해서 정보를 조회해 올 수 없냐는 것이다.

그것을 지원하기 위해서 C# 3.0이상의 버전에서는 LINQ라는 이름의 쿼리문을 제공하고 있다. 즉  데이터 저장 장소가 어떻게 되든 LINQ 라는 쿼리문을 사용하면 동일한 표현으로 작성할 수 있다는 것이다. 개발자는 C#용 표현, SQL 표현, XML용 표현을 작성하지 않아도 된다는 것이다.

var query =

                from c in customers

                where c.Discount > 3

                orderby c.Discount

                select new { c.Name, Perc = c.Discount/100 };

from부터 select까지가 LINQ문이다. 여러 고객들중에서 할인(Discount) 서비스를 3 이상으로 받는 고객을 Discount순으로 정렬해서 이름과 할인율을 가져온다는 것으로 대충(!) 해석할 수 있다. 물론 이게 무슨 말인지 아직은 모른다. 우리말을 모르겠다는 것이 아니라 C#표현이 이해가 가지 않는다.

제일 궁금한 것은 select문 이하이다. 일단 select를 하면 여러 건수가 검색될텐데 그럼 컬렉션이 리턴된다는 말인데...음... 그리고 이 건수들의 타입 즉 Name과 Perc를 정보를 갖는 객체가 어떤 타입일지 모르겠다. 그리고 알 수 없는 타입의 객체들의 컬렉션을 받는 query라는 var 타입의 변수의 정체도 모르겠다. Javascript의 Variable변수 var와 비슷해 보여서 어느 정도 감은 있다. 즉 어떤 타입의 객체도 받을 수 있을 것 같긴 한데, 여튼 이것도 미지근한 존재임은 사실이다.

그리고 고객 정보의 컬렉션을 query에 할당했다면 어떻게 각각의 고객의 속성 예를 들어 Name, Perc에 접근할 수 있는지도 궁금하다.

■ 배워야 할 것들!!

그렇다. LINQ의 표현에 대해서 모른다. 그래서 알아보려고 한다. 그러나 사전에 배워야 하는 것들이 많다.  앞의 표현을 C#3.0은 다음처럼 해석한다.

var query = customers

                   .Where ( c => c.Discount > 3 )

                   .OrderBy( c=>c.Discount )

                   .Select ( c=> new { c.Name, Perc = c.Discount /100 } );

이 중에서 "=>" 표시를 사용한 부분을 람다 표현(lamda expression)이라고 한다. 이 람다 표현은 다시 이전 문법의 익명 메소드, 델리게이트 등으로 다시 바꿔 표현할 수 있다. 나중에 보여준다. 지금 말하고 싶은 것은 LINQ 표현은 이렇게 이렇게 정의하자고 해서 단박에 정의된게 아니다라는 것이다.  앞의 from과 select 사이의 표현이 나오기까지는 많은 진화 과정을 거쳤왔다. 그 진화 과정을 이해해야 LINQ 표현을 일고 제대로 해석할 수 있게 되는 것이다.

우선 그 진화 과정에 대한 설명이 1차 목표이다. 표현의 진화 과정을 설명하기 위해서는 C#2.0의 제너릭(generic), 델리게이트(delegate)를 거쳐서 익명 메소드(anonymous method) 그리고 람다 표현(lamda expression), 타입 추론(type inference), 확장 메소드(extension methods) 휴~~ 그리고 기타등등 헉헉~~등 많은 개념들을 알아야 한다.

그리고 나서 이제 본격적으로 LINQ의 연산자들을 정리하게 된다. 앞의 표현에서 from, where, orderby, select등은 LINQ문의 여러가지 연산자중의 일부이다. 그 외에 많은 LINQ 연산자들이 있다. 이미 익숙해있는 SQL문에서 표현할 수 있는 그런 연산자들을 정리한다고 생각하면 된다. Join, Union, Intersect, Distinct 등등.

따라서 진화 과정에 대한 이해만 이뤄지면 LINQ문은 쉽게 정리가 될 것으로 보인다.

■ 포스트 진행 방향

필자도 아직 정리가 되지 않은 새로운 개념들과 표현도 있다. 되도록이면 메뉴얼 냄새가 나지 않도록 풀어서 일상적인 용어와 예를 통해서 설명이 되도록 최선을 다하고 싶다. 그러다 안되면?......말고! 왜? ..... 내 블로그니까!!

Posted by dalbong2
TAG LINQ