요즘은 달봉이가 맡았던 업무를 전산실 직원에게 백업을 하는 것이 주요 하루 일과다. 오늘 백업도중 어쩌다 .NET의 이벤트 핸들링 패턴에 대한 얘기가 나왔었다. 예전에 달봉이가 처음 이벤트를 배울려고 했을때 잘 이해하지 못했던 부분이 있었는데, 생각난 김에 함 정리를 해 볼려고 한다.

우리는 어떤 이벤트를 이용하기 위해서 사용하는 방법에 대해서 알고 있다.
- 이벤트( 멤버)에 이벤트 핸들러를  등록하거나
- 또는 해당 이벤트 On이벤트 메소드를 오버라이드하기

이 두 방법의 차이점은 무엇일까? 이 두 방법을 모두 이용할 수 있는 것은 .NET의 이벤트 핸들링 패턴 때문이다. 다음 코드는 이벤트 "Loaded"라는 것을 정의해서 이 이벤트에 대한 핸들링을 하는 두 방법을 보여주고 있다. 다시 한번 상기시키면 다음 코드는 개발자가 작성하는 코드이다.


 
public class DerivedControl : BaseControl
  {
     
public DerivedControl()
      {
         //이벤트 핸들링 방법 2.1
         
this.Loaded += new LoadedEventHandler(this.LoadedEventHandler);
     
}
     //이벤트 핸들링 방법 1
     
protected override void OnLoaded(EventArgs e)
      {
         
//이곳에서 필요한 작업을 한다.

          //베이스의 OnLoaded()를 호출한다.
         
base.OnLoaded(e);
     
}
      //이벤트 핸들링 2.2
     
private void LoadedEventHandler(object sender, EventArgs e)
      {
         
//이곳에서 필요한 작업을 한다.
     
}
  }

개발자가 DerivedControl을 가지고 작업을 할때 이벤트 핸들링 1 방법을 사용해도 되고 2 방법을 사용해도 된다. 

이벤트 핸들링 1 : On메소드 오바라이딩 사용.
이벤트 핸들링 2 : 이벤트 핸들러 사용

이벤트 핸들러를 사용하는 방법은 2.1과 2.2 가 필요하다. 즉 핸들러 메소드를 제작(2.2)해야 하고  이 핸들러를 해당 이벤트에 등록(2.1)해야 한다.

이벤트 핸들러를 사용하는 것에 대한 더 이상의 얘기는 하지 않겠다. 여기서는 On메소드 오버라이딩 방법을 사용했을 경우 어떻게 이 DerivedControl.OnLoaded() 메소드가 호출되는지를 알아볼 것이다. 그러기 위해서는 베이스 클래스 BaseControl의 구조를 머리에 그릴 수 있어야 한다.  지금부터 함 BaseControl을 그려보자.

 
//Loaded이벤트용 델리게이트 정의
  
public delegate void LoadedEventHandler(object sender, EventArgs e);

  
//베이스 클래스
  
public class BaseControl  : System.Windows.Forms.UserControl
   {
       //이벤트 멤버 : Loaded
      
public LoadedEventHandler Loaded = null;
       protected virtual void
OnLoaded(EventArgs e)
       {
          
if (Loaded != null)
           {
              //이벤트 발생
               Loaded(
this, e);
          
}
       }
   }


베이스 클래스 BaseControl에는 이처럼 이벤트 Loaded와 그것을 호출하는 가상 메소드 OnLoaded()가 정의되어 있을 것이다.

.NET 이벤트 발생 패턴에서는 직접 Loaded를 호출하는 대신에 이렇게 On이벤트 메소드를 제공해서 이 메소드내에서 이벤트를 호출하도록 하고 있다.

컨트롤이 로딩되고 나서 프레임워크쪽 또는 BaseControl쪽의 어디에선가 Loaded 이벤트를 발생시키기 위해서 OnLoaded()를 호출하게 된다. 예를 들어 다음과 같은 코드가 있을 수 있다.

private void BaseControl쪽의메소드(...)
{
....
//이곳은 BaseControl쪽 코드
this.OnLoaded(e);  // 순전히 예를 위한 호출이다.
}


이제 개발자가 다음과 같은 코드를 작성했다고 보자.

BaseControl ctrl = new DerivedControl();
윈폼객체.Control.Add(ctrl);

이렇게 생성된 컨트롤 ctrl을 윈폼이나 컨테이너 컨트롤에 Add 시켜서 로딩시켰다고 하자. 그리고 Loaded 이벤트를 발생시켜야 하는 시점에서 즉 BaseControl쪽의메소드() 내부에서 this.OnLoaded(e) 호출했다고 하자. BaseControl의 OnLoaded()가 호출될까?

BaseControl.OnLoaded()가 호출되는 것이 아니라 DerivedControl.OnLoaded()가 호출된다. this가 가리키는 것은 new DerivedControl() 객체이기때문이다.

즉 우리가 원하는 이벤트 핸들링이 이뤄진것이다.

호출되는 DerivedControl.OnLoaded()의 내용을 보면 코드에서처럼 그곳에서는 다시 베이스의 OnLoaded()를 호출해야 한다.

//베이스의 OnLoaded()를 호출한다.
base.OnLoaded(e);


이 메소드를 호출해야 base.OnLoaded()에서 Loaded 이벤트를 호출해서, 다른 곳에서 Loaded에 등록했을지도 모를 다른 핸들러들을 실행시켜야 하기 때문이다.

따라서 메소드 실행 순서를 정리해보면 다음과 같다.

DerivedControl.OnLoaded() -> BaseControl.OnLoaded() -> Loaded 이벤트 핸들러

이벤트 핸들링 하는 방법으로 On메소드을 오버라이딩하는 것과 이벤트 핸드러를 사용하는 것에 대해서 알아봤다.

다 써놓고 보니까 또 내가 이것을 왜 썼나 싶다.-_-;; 히잉~
Posted by dalbong2

C# 2.0 iterators

개발/C# 2009/04/23 21:11

벌써 C#이 버전 3.0까지 나왔다는데, 달봉이는 아직도 2.0 버전에 당황을 하곤 한다.
지금 소개할 문법도 오늘 아침 처음으로 본 것이다.

using System;
using System.Collections.Generic;
class Test
{
  public static void Main()
  {
       foreach (string s in GetItems())
           Console.WriteLine(s);
  }

  private static IEnumerable GetItems()
  {
       yield return "Hello yield 1";
       yield return "Hello yield 2";
       yield return "Hello yield 3";
       yield return "Hello yield 4";
       yield return "Hello yield 5";
  }
}

"yield return" 문이 있는데, iteration의 다음 값을 반환한다고 한다.
"yield break" 문도 있단다.

『The yield return statement produces the next value of the iteration.
  The yield break statement indicates that the iteration is complete.』

GetItems() 메소드의 리턴 타입이 IEnumerable 이라는 것도 주목할 부분이다.


참조 문서
c# 2.0 iterators -
http://community.bartdesmet.net/blogs/bart/archive/2006/07/06/4121.aspx

Posted by dalbong2

앞에서 쿼리 표현식의 where는 C#의 Where 메소드로 변환된다고 했다. 즉 LINQ 쿼리는 몇 개의 쿼리용 오퍼레이터로 구성된다. 그리고 그 쿼리용 오퍼레이터는 IEnumerable<T>를 구현하는 어떤 타입들의 확장 메소드들이다. IEnumerable<T>를 구현하는 모든 타입의 객체에 대해서 Where 메소드를 호출할 수 있다는 것이다.

LINQ 쿼리가 이런 확장 메소드들을 기반으로 하고 있다는 것은 매우 확장적이다. 확장스럽다. 확장답다^^ 확장!확장! 기본적으로 지금 C#3.0에서부터 제공하고 있는 쿼리용 오퍼레이터들은 IEnumerable<T>를 확장한 메소드들이다. 현재도 많은 리스트 타입들이 IEnumerable<T> 인터페이스를 구현하고 있기때문에, 이런 확장 메소드를 사용하는 방법은 LINQ를 일반적인 쿼리용 프레임워크가 될 수 있게 한다.

그러나 쿼리 대상이 되는 데이터가 다른 타입이라면 그 타입에 맞는 확장 메소드들만 제공하면 얼마든지 확장 즉 특화가 가능하다는 것이다. LINQ to SQL, LINQ to XML은 관계형 데이터와 XML 노드 데이터를 핸들링하기 위한 특화된 확장 메소드들을 사용하고 있다. 지금까지 설명하면서 사용했던 IEnumerable<T> 타입의 확장 메소들은 LINQ to Objects 용 메소드들이이었다. 이쯤해서 얼떨결에 배웠던 확장 메소드가 LINQ에서 얼마나 중요한 개념인지를 알 수 있게 된다. 확장 메소드에 대한 포스트를  다시 한번 더 훑어봐도 시간낭비일 것 같지 않은 기분이 팍팍든다.

앞에서 LINQ to Objects용 확장 메소드들은 System.Linq 네임스페이스에 있는 정적 클래스 Enumerable에 정의되어 있다고 했었다. 앞에서도 말했지만 쿼리 표현식은 모두 C#의 확장 메소드로 변환시킬 수 있다. 그러나 모든 확장 메소드를 LINQ 쿼리 표현으로 변환할 수 있는 것은 아니다. 따라서 두가지 표현을 모두 이해하는 것이 바람직하다.

이 포스트에서는 조인과 그룹핑 그리고 정렬 정도의 기본 개념을 어떻게 표현하는지를 알아보려했으나 이제 좀 지겹다. 그래서 다음 한용희 MVP님의 포스트를 소개하겠다.  동영상이 많다. 좋은 블로그이다.

http://blog.naver.com/woom333/60047172335

다음은 영어로 되어 있는 포스트이다.

Express Yourself with C#'s Query Syntax(http://visualstudiomagazine.com/columns/article.aspx?editorialsid=2461)

Posted by dalbong2

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

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

앞의 포스트에서 Object initializer에 대해서 알아봤다. 먼저 읽어보는 것이 좋을 듯 싶다. 이제 익명 타입(anonymous types)을 알아보자. C#3.0부터는 다음과 같은 표현이 가능해진다.

Customer c1 = new Customer{ Name="달봉이"};

var c2 = new Customer{Name="봉달이"};

var c3 = new {Name="봉봉이", Age=30 };

var c4 = new {c2.Name, c2.Age };

var c5 = new { c1.Name, c1.City};

var c6 = new {c1.City, c1.Name};

c1 생성은 앞에서 배운 객체 초기화 코드이다. c2는 Customer 객체를 초기화해서 var 타입에 할당하고 있다. var 키워드가 사용되면 할당된 표현식으로부터 변수의 타입을 유추해낸다. 해서 c2는 Customer 타입이라는 것을 알아낸다.

근데, c3,c4,c5,c6이 이상하다. 타입이름이 없다. 그렇다. 타입 이름이 없다. 굳이 타입 이름을 밝히지 않아도 되는 경우가 있다는 것이다. 그런 경우 굳이 이름을 지을 필요는 없다. 이런 경우는 컴파일러가 내부적으로 타입 이름을 지어주고 var의 타입 결정단계에서는 내부에서 생성한 타입으로 결정된다. 내부에서 자동 생성된 타입에는 initializer에서 사용된 공개 속성 또는 멤버에 해당하는 공개 속성과 해당 전용 멤버가 정의된다.  그 공개 속성과 내부 전용 멤버의 이름과 타입은  initializer에서 사용된 예를 들어 {Name="봉봉이", Age=30}를 통해서 유추된다.

c1~c6의 타입을 출력해보면 다음과 같다.

Console.WriteLine("c1 is {0}", c1.GetType());

Console.WriteLine("c2 is {0}", c2.GetType());

Console.WriteLine("c3 is {0}", c3.GetType());

Console.WriteLine("c4 is {0}", c4.GetType());

Console.WriteLine("c5 is {0}", c5.GetType());

Console.WriteLine("c6 is {0}", c6.GetType());

1155834665

LINQ 쿼리에서는 코드상에서 타입 이름은 필요없고, 단지 공개된 속성을 통해서 객체에 대한 쿼리만이 필요하다. 예를 들어 Name이 "달봉이"인 객체를 조회하는 쿼리문에서 타입이 뭔지는 필요없다. Name이라는 속성이 공개되어있고 그 타입이 string이면 된다.

앞의 포스트에서 C#1.x에서도 다음과 같은 표현이 가능했다고 했다.

string[] names = {"달봉이", "봉달이", "봉봉이"};

C#3.0에서는 배열을 초기화하는 표현이 var, 익명 타입과 object initializer를 이용해서 훨씬 강력해졌다.

var c = new [] {

    new { Name="달봉이", Sports=new[] { "테니스", "축구"} },

    new { Name="봉달이", Sports=new[] { "럭비", "골프"} },

    new { Name="봉봉이", Sports=new[] { "숨쉬기", "뒹굴기"} }

};

변수 c는 배열인데, 그 요소의 타입은 모두 이름이 없고 다만 속성으로 string 타입의 Name과 string배열의 Sports를 갖고 있는 요소들이다. 이 표현을 통해서 내부적으로는 타입의 이름보다는 그 타입의 구조만 알 수 있으면 된다는 것이다. 또한 주목할 것은 이 모든 표현이 한줄의 문장으로 이뤄졌다는 것이다. 즉 이런 표현이 쿼리 표현식 내에 표현될 수 있는 가능성이 확보된 것이다. 그런 의미에서 다시 한번 더 쿼리 표현식을 보자.

var query =

    from c in customers

    where c.Discount > 3

    orderby c.Discount

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

select 이하에서 익명 타입을 사용한 표현이 있다는 것을 알 수 있다. ^^

휴~~이제 거의 다 온 건가. 이제 마지막으로 람다 표현식에 대해서 알아보자.

Posted by dalbong2

현재 시리즈 제목 "LINQ시리즈"이다. 그러나 아직 본격적인 LINQ에는 들어가지도 못하고 있다. 지금 하나씩 설명하고 있는 단위 기술들 델리게이트, 익명 메소드, 제네릭, 타입 유추, 익명 타입 그리고 앞으로도 배울 람다 표현식을 포함한 몇 가지는 그 자체만으로도 가치가 있는 기술들이기는 하지만 뒤에서 설명할 LINQ에서 조합되어서 그 효과를 발휘하게 될 것이다. 그래서 표현식은 아조 아조 심플하게 변하게 된다. 한번 더 볼까나.

var query =

    from c in customers

    where c.Discount > 3

    orderby c.Discount

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

이렇게 간단한 표현속에 그렇게 많은 개념과 기술이 들어가 있을 줄이야. 필자도 미처 몰랐다.  이젠 좀 지겹기는 하다.  할것도 많은데. LINQ에 대한 본격적인 공부도 해야 하고. 그리고 필자의 원래의 목표였던 프레임워크 주제 특히 이번에는 Spring.NET에 대한 공부도 계속 하야 하는데. 그러나 이 표현식을 그냥 대충 넘어갈 수는 없을 것 같다. 완벽히 이해해야 할 것 같다. 그래야 앞으로의 개발자 생활이 편해질 것이라는 것이 필자의 동물적 생존 본능으로 느껴지고 있다. 빠샷!

오늘 Object initializer, 익명타입(anonymous type)이란 것을 함께 알아보자. 이 두 녀석도 철저히 코드를 심플하게 만들기 위한 개념들이다. 객체를 생성할때 그 내부 상태를 특정 상태로 초기화시켜 주고 싶다면 보통 파라미터가 있는 생성자(Constructor)를 사용합니다.  다음과 같은 타입이 있다고 하겠다.

public class Customer

{

    public string Name;

    public string City;

    public int Age ;

    public Customer(){}

    public Customer(string name, int age)

    {

        this.Name = name;

        this.Age = age;

    }

}

Customer의 객체는 name, age 파라미터를 갖는 생성자를 통해서 초기화될 수 있다. 만약 age가 아니라 city값을 초기화하고 싶다면 앞의 생성자 대신에 파라미터가 없는 기본 생성자와 공개적으로 노출된 속성을 통해 설정하는 다음과 같은 코드가 필요하다.

Customer c = new Customer();

c.Name = "Bart";

c.City = "Ghent";

C#3.0부터는 이렇게 객체를 초기화하는 코드를 간단히 할 수 있는 폼을 제공하고 있다.

Customer c = new Customer{ Name = "달봉이", City="Seoul" };

이런 표현을 Object initializer라고 한다. 인자가 없는 기본 생성자와 상태 설정이 필요한 값을 공개 속성 또는 필드로 노출시켜 두면 된다. 이 코드는 컴파일되어 IL코드로 되면 앞의 코드의 컴파일 결과와 동일해진다. {}사이에 명시된 이름들은 초기화되는 객체에서 공개적으로 노출한 속성 또는 필드에 해당한다.

앞에서처럼 타입과  시작 브래킷{ 사이에 ()이 없는 경우는 기본 생성자를 호출한다. 그러나 인자가 있는 생성자를 호출할 수도 있다.

// 파라미터가 있는 생성자를 초기화에 사용할 수도 있다.

Customer c = new Customer( "달봉이", 100 ) { City="Seoul" };

object initializer를 사용하면 함수 형태로 객체 초기화를 마무리할 수 있다. 즉 다른 문장을 사용하지 않고도 하나의 문장으로 복잡한 초기화를 끝낼 수 있다. 다음 코드를 보자.

Customer c = new Customer{

    Name = "Bart",

    Age = 23,

    Address = new Address {

                   Street = "Andersstreet",

                   Number = 60,

                   PostalCode = 9000,

                   City = "Ghent"

                }

  };

이렇게 객체안에 포함된 다른 객체의 초기화 코드도 하나의 문장으로 마무리될 수 있다. 이 표현도 다시 간단하게 변할 수 있는데, 내부의 중첩된 객체의 생성에서는 new를 없앨 수 있다. 최외곽의 생성자에서만 new를 사용해도 된다.

Customer c = new Customer{

    Name = "Bart",

    Age = 23,

    Address = {

                    Street = "Andersstreet",

                    Number = 60,

                    PostalCode = 9000,

                    City = "Ghent"

                }

  };

C#1.X에서도 이런 비슷한 표현이 있었다. 그러나 배열 생성에만 제한되어 있었다.

string[] names = {"달봉이", "봉달이", "봉봉이"};

이런 표현의 initializer는 컬렉션에서 자주 사용된다.

List<Customer> list = new List<Customer>{

    new Customer("달봉이", 100){ City = "Seoul"},

    new Customer{Name="봉달이"},

    new Customer{ Name = "봉봉이" City = "Seoul"},

};

요약하면, LINQ에서는 주로 그 쿼리의 대상들이고 그리고 그 쿼리 결과도 컬렉션으로 반환되는 경우가 많다. object initializer 표현은 LINQ에서 광범위하게 사용된다.  익명 타입(anonymous type)은 objct initializer로 인해서 심플해진 표현을 더 심플하게 해 준다.

포스트가 너무 길어진다. 익명 타입은 다음 포스트로 넘기자.

Posted by dalbong2

C#(3.0이상)의 타입 유추(type inference)는 쿼리 표현을 단순하게 만드는데 있어서 핵심적인 역할을 하는 기능중의 하나이다. 쉽게 말하면 변수의 타입을 정확히 명시하지 않고도, 앞 뒤 표현 문맥을 통해서 그 변수의 타입을 유추해 낼 수 있는 기능이다. 이 기능을 이용하면 변수의 타입에 대해서는 좀 덜 명확하게 되기는 하지만 "코드"가 좀 더 자연스런 언어처럼 된다. 여기서 "코드"란 쿼리 표현(query expression)을 말한다. 즉 쿼리 표현에서 처럼 자연스러움 즉 읽기 편함(readability)이 중요하고 명확한 타입 선언이 반드시 필요한 곳이 아니라면 타입 유추 기능은 의미를 갖게 된다. 사실 타입 유추 자체만으로는 그렇게 큰 의미가 없는 듯하다. 그러나 언어의 다른 기능과 함께할때 그 중요성? 편의성?은 커지는것 같다.

C#에서 타입 유추 메커니즘이 작동하는 경우는 2가지이다. var 타입의 변수를 사용할때와 그리고 제네릭을 사용할때 <T>을 생략하는 표현을 사용할 수 있는데 이때에 타입 유추가 수행된다.

■ var 타입의 변수 , local type inference

우선 var 키워드의 사용에 의한 타입 유추에 대해서 알아본다.다음과 같은 표현을 보자.

var a = 2;      //  a는 int형으로 선언된다.

object b = 2;   //  정수 2를 object로 박싱시킨다.

int c = a;      //  타입 변환 NO! 언박싱 NO!

int d = (int)b; //  타입 변환 필요! 언박싱 필요!

앞의 코드에서 var 타입의 변수를 사용했다고 해서 표현이 읽기 편해지고, 더 자연스럽게 되는 것은 아니다.  그러나 var가 뒤에서 배울 익명 타입(anonymous types)에서 사용될 수 있고 그리고 익명 타입이 쿼리 표현에서 사용된다는 것을 알게 되는 시점에서는 var가 표현을 간단히 해 줄 수 있다는 것을 알게 된다. 앞의 예제 코드에서는 타입 유추가 뭔지를 이해하는 것이 더 중요한 목적이라고 보면 된다.

var 타입은 분명 object 타입처럼 모든 타입의 변수로 될 수 있지만 object와는 다르다. C#이 코드를 컴파일할때, var 타입의 변수는 그 "주변의 표현"을 근거로 해서 변수의 타입을 유추한다. 그래서 IL 코드가 되는 순간에 var 타입의 변수는 구체적인 타입으로 변하게 된다. 앞의 변수 a에 값을 할당하는 코드는 IL코드가 되면 다음과 같은 코드와 동일한 코드가 되는 것이다.

int a = 2;

반면에 변수 b는 컴파일시에도 그 타입이 object로 남게 된다. 그래서 마지막 라인처럼 런타임시에 실제의 타입 int로의 변환이 필요하고 이때 언박싱이 수행된다. 따라서 좀 어려운 말로 하면 var를 이용하는 변수 선언은 type-safe 선언이라고 할 수 있다. 따라서 다음 코드는 컴파일시에 에러로 결정될 수 있다는 것이다. 컴파일시에!

int z = 0;

int y = 1;

var a = z + y;

a = "2"//  에러 !!

이 경우는 "int변수 + int변수"라는 표현을 통해서 a의 타입을 int로 유추해낸다. 마지막 코드에서 int 변수 a에 문자열 "2"를 할당하고 있다. 에러이다. 이렇게 로컬 변수의 타입을 결정할때 일어나는 타입 유추 과정을 특히 local type inference라고 부른다. 제네릭 타입의 유추와 비교할 수 있는 용어이다.

var 타입의 변수는 로컬 영역에서만 사용할 수 있다. 로컬 변수만 var로 정의될 수 있고, 클래스 멤버 나 파라미터는 var로 정의될 수 없다.  다음은 var를 사용한 몇 가지 유효한 코드의 예이다.

 public void ValidUse(decimal d)

{

    var x = 2.3;            // x 타입 : double

    var y = x;              // y 타입 : double

    var r = x / y;          // r 타입 : double

    var s = "sample";       // s 타입 : string

    var l = s.Length;       // l 타입 : int

    var w = d;              // w 타입 : double

    var p = default(string);// p 타입 : string

}

그러나 다음 코드는 var를 잘못 사용한 경우의 예이다.

class VarDemo

{

    // 클래스, 구조체, 인터페이스의 멤버 선언에 사용할 수 없다.

    var k = 0;


    // 파라미터 타입으로 사용할 수 없다.

    public void InvalidUseParameter( var x){}


    //반환값의 타입으로 사용할 수 없다.

    public var InvalidUseResult()

    {

        return 2;

    }


    public void InvalidUseLocal()

    {

        var x;        // "="할당이 필요하다.

        var y = null; // "null"로부터는 타입 유추를 할 수 없다.

    }

}

var 는 타입의 멤버 그리고 메소드의 파라미터에 사용될 수 없다는 것을 보여주고 있다. 그리고 InvalidUseResult()에서는 반환값이 2이므로 반환값 타입이 int라는 유추를 하는데는 무리가 없지만, 반환값의 타입으로 var는 허용되지 않는다. 이처럼 멤버나 파라미터, 반환값에 var를 허용하지 않음으로 해서 상속이나 오버로딩에 의해서 복잡해질 수 있는 문제를 막을 수 있게 되는 것이다. 예를 들어 파라미터 타입이 다른 오버로드 버전의 메소드를 정의해서 호출한다면 어떤 메소드가 호출되어야 하는지가 분명하지 않게 된다.

public void ExMethod(var x){ }

public void ExMethod(int i){ }

이렇게 정의한 두 버전의 오버로드가 있을때 ExMethod(2)를 호출하면 어떤 버전이 호출되겠는가. 이런 혼란을 허용하지 않겠다는 것이다.

그리고 마지막 InvalidUseLocal() 메소드에서는 var가 로컬 변수에서 사용되는 경우에도 허용되지 않는 경우를 보여주고 있다. 요는 로컬 변수에 사용될때에도 반드시 타입을 유추할 수 있는 표현이 필요하다는 것이다. 

■제네릭 타입의 유추

다음과 같은 제너릭 메소드를 정의했다고 해 보자.

T Min<T>(T a, T b) where T : IComparable<T>

{

    if (a.CompareTo(b) < 0)

        return a;

    else

        return b;

}

Min() 메소드는 타입 파라미터(type parameter) T와 같은 타입의 두 개의 파라미터 a, b를 받는다. 이때 타입 T는 IComparable 인터페이스를 구현해야만 한다는 것을 where이하에 표현하고 있다. 메소드 바디의 구현을 보면 이런 두 타입의 변수를 받아서 작은 값을 반환한다는 내용이다.

이 제네릭 메소드는 다음과 같은 모양으로 호출할 수 있다.

int a = 5;

int b = 10;

int c = Min<int>( a, b );

<int>를 통해서 파라미터 타입과 반환값 타입이 이미 밝혀졌기때문에 C# 컴파일러는 이 메소드를 다음과 같이 해석할 수 있기 때문이다.

int Min<int>( int a, int b ){ }

이것은 Min<int>()을 호출하고 나서 그 결과를 int로 타입변환하지 않아도 되는 이유이다.

제네릭 타입 T를 유추하기 위해서 앞의 코드에서는 <int>의 타입 파라미터 int를 이용해서도 알 수 있겠지만, 파라미터 a, b의 타입을 통해서도 가능하다. 즉 int a = 5; int b =10;을 통해서 a,b의 타입이 int형으로 밝혀지게 되고 그렇게 되면 타입 파라미터와 반환값의 타입 T도 밝혀지게 된다. 따라서 굳이 이 메소드를 호출할때 Min<int>(a, b)로 표현하지 않고도 다음처럼 호출해도 상관없게 된다.

int c = Min( a, b );

타입 유추라는 언어의 기능때문에 제네릭 메소드 Min<T>()의 호출에서 타입 파라미터 T가 생략되어 표현이 간단해지고 있다.

그럼, 다음 코드는 어떻게 될까 궁금해진다. 

static void Main(string[] args)

{

    int a = 0;

    int b = 1;

   int c = Min(a, b);

    Console.Read();

}


static T Min<T>(T a, T b) where T : IComparable<T>

{

    Console.WriteLine("Generic method called");


    if (a.CompareTo(b) < 0)

        return a;

    else

        return b;

}


static int Min(int a, int b)

{

    Console.WriteLine("General method called");

    if (a < b)

        return a;

    else

        return b;

}

Min(a, b)은 Min<T>()을 호출하는 것도 가능하고 Min( )을 호출하는 것도 가능하다. 그럼 어느 것이 우선권을 가질까? 아니면 우선권이 같기때문에 컴파일러는 에러를 내뱉을까? 실제로 이 코드를 실행시켜보면 제네릭 메소드보다 구체적인 타입의 메소드가 먼저 호출된다.

여튼 지금부터 이제 본격적으로 쿼리 표현에 가까워지게 될 것이다. 앞에서 계속 봐온 쿼리 표현이이다.

Customer[] customers = GetCustomers();

var query =


from c in customers


where c.Discount > 3


orderby c.Discount


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

이 표현에서 c 변수에 대한 타입은 명시되지 않고 있다. 그러나 앞뒤 표현을 통해서 C# 컴파일러는 c의 타입이 Customer라는 것을 유추해낸다.

C#이 이 쿼리 표현을 어떻게 해석하는지 다시 한번 더 보자. 

var query = customers


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


       .OrderBy( c=>c.Discount )


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

var 타입의 변수 query에 어떤 값이 할당될까? customers 객체의 메소드가 차례로 호출되고 나서 마지막으로 Select()가 호출되고 나면 IEnumerable<T> 인터페이스를 구현하는 타입의 인스턴스가 반환된다. 여기서 타입 T는 순환의 대상이 되는 컬렉션의 요소의 타입을 나타낸다.  앞의 쿼리문에서 T는 구체적으로 Customer가 된다.

보니까 이 표현을 모두 설명하기 위해서 앞으로도 꽤 많은 것을 설명해야 할 것 같다. 람다 표현식("=>"을 포함한 표현), new {c.Name, ....} 부분을 익명 타입(anonymous type)이라고 하는데 이것도 알아봐야 하고....우 언제 끝나려나..이것을 다 설명하고도 본격적으로 LINQ에 대해서 설명해야 하는데....언젠가는 끝나겠지...


추가

글을 게시해놓고 보니까 type inference에 대한 요약이 없는 듯해서 다시 보충한다.

타입 유추(Type inference)라는 것은 컴파일러가 어떤 값의 타입을 주변의 표현식을 평가해서 자동으로 유추해낼 수 있는 능력을 말한다. 타입이라 하면 int, string같은 기본적인 타입뿐만 아니라 사용자 정의의 타입도 자동으로 만들어 낼 수 있다. 뒤에서 익명 타입(anonymous types)에 대한 설명이 나오겠지만 사용자 정의 타입을 만들때 이름을 주지 않고도 코드 상의 일부로 정의할 수가 있다. 마치 익명 메소드처럼. var 변수는 이런 익명 타입의 인스턴스도 받을 수 있는데, 컴파일러는 이런 인스턴스에 대한 타입을 컴파일시에 정의해서 이름도 내부에서 만들어 낸다. 이런 타입에 대한 평가와 결정 작업이 모두 컴파일시에 일어난다는 것을 기억할 필요가 있다. 즉 컴파일후 IL이 된 상태에서는 이미 내부적으로 타입이 결정되어 있는 상태가 된다.  컴파일시에 명확한 타입 표시가 없으면 컴파일러가 그 타입에 대한 평가를 시작하게 되는데, 컴파일러의 이런 타입 평가 시스템이 강력할 수록 프로그램이나 언어는 그만큼 더 간결해 질 수 있게 되는 것이다.

명확한 타입 표시가 없는 표현식에서 값에 대한 타입을 정확히 유추해내기 위해서 컴파일러는 주변 하위 표현식에서 주어진 타입 표시를 계속적이고 반복적인 작업을 통해서 미정의 타입에 대한 확률을 높여간다. LINQ 쿼리문에서의 하위 표현식이 간단치만은 않을 수 있다. 이런 쿼리문에서 특정 값의 타입을 유추해 내려면 필자도 잘은 모르겠지만, 내부적으로 아마 복잡한 타입 유추 알고리즘이 돌아가야 할 것이다.

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