본문 바로가기

IT 살이/04. 기술 - 프로그래밍

LINQ 시리즈 09 - type inference(타입 유추) 좀 더

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

타입 유추가 왜 일어나야 하는가. 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>가 된다.

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