[제목] C# delegate의 내부 구조에 대하여 II (MultiCast)
지난 SingleCast C# delegate 내부구조 아티클에 이어 이번에는 복수개의 델리게이트를 가진 MultiCast C# delegate의 내부 구조를 살펴보자.
(주: C# delegate의 내부 구조에 대하여 I (SingleCast) 을 읽지 않은 분들은 먼저 그 아티클을 읽기 바랍니다.)
.NET의 Delegate 클래스는 Combine()이라는 메서드를 제공하는데, 이 메서드는 2개의 Delegate 객체를 하나로 합치는 기능을 제공한다. C#에서 이와 비슷한 기능으로 좀더 편리한 += 연산자를 사용할 수 있는데, 이 연산자를 기존 Delegate 객체에 계속 += 연산자 이후의 Delegate 객체를 추가하게 된다. 다음 예제는 3개의 메서드를 m 이라는 Delegate 객체에 계속 추가하는 코드를 보여주고 있다.
class Program { public delegate void MyDelegate(); static void Main(string[] args) { // Class1은 [C# delegate 내부구조 1] 참조 Class1 c = new Class1(); MyDelegate m = null; // 1개의 메서드 레퍼런스 m += c.InstRun; // 2개의 메서드 레퍼런스 m += Class1.StaticRun; // 3개의 메서드 레퍼런스 m += () => Console.WriteLine("*"); Console.ReadLine(); // 3개를 순서대로 호출 m(); // GetInvocationList()을 사용하여 // Delegate를 하나씩 처리할 수도 있다. foreach (Delegate d in m.GetInvocationList()) { Console.WriteLine(d.Method.Name); } } }
위의 예에서 첫번째 메서드는 Class1의 Instance 메서드이고, 이때는 SingleCast Delegate로서 이전 아티클에서 보았듯이 _target과 _methodPtr 필드를 사용한다. 이어 두번째 Class1.StaticRun 메서드가 추가되면, 이는 MultiCast Delegate가 되면서, 마지막 2개의 필드를 사용한다. 즉, 6번째 필드인 _invocationCount이 2가되고, 5번째 필드인 _invocationList 필드에 컬렉션으로서 2개의 델리게이트 객체를 갖는다.
위의 코드에서 3개가 모두 추가된 후, MultiCast 델리게이트 객체가 된 후에 Console.Readline()이 호출된다. Windbg 디버거를 통해 Console.ReadLine() 에서 중지하고 !clrstack 을 실행하면 맨 마지막에 아래와 같은 Main() 메서드의 로컬 변수들을 볼 수 있다.
0:000> !clrstack -a ...생략... 0036efec 001e01e5 *** WARNING: Unable to verify checksum for DeleApp.exe DeleApp.Program.Main(System.String[]) [c:\Dev\DeleApp\Program.cs @ 21] PARAMETERS: args (0x0036f01c) = 0x02772250 LOCALS: 0x0036f018 = 0x02772260 0x0036f014 = 0x02772324
두번재 로컬변수 0x02772324 을 살펴보면, 이것이 MyDelegate 객체임을 알 수 있고, 이 멀티캐스트 델리게이트 객체가 3개 (_invocationCount)의 메서드 레퍼런스를 가지고 있음을 알 수 있다.
0:000> !do /d 0x02772324 Name: DeleApp.Program+MyDelegate MethodTable: 00143904 EEClass: 00141368 Size: 32(0x20) bytes File: C:\Dev\DeleApp\bin\Debug\DeleApp.exe Fields: MT Field Offset Type VT Attr Value Name 5d44b064 400002d 4 System.Object 0 instance 02772324 _target 5d44b064 400002e 8 System.Object 0 instance 00000000 _methodBase 5d44a0e8 400002f c System.IntPtr 1 instance 13a01c _methodPtr 5d44a0e8 4000030 10 System.IntPtr 1 instance 1438c0 _methodPtrAux 5d44b064 4000031 14 System.Object 0 instance 02772304 _invocationList <== 컬렉션 5d44a0e8 4000032 18 System.IntPtr 1 instance 3 _invocationCount <== 갯수
_invocationList 리스트를 출력해 보기 위해 아래와 같이 SOS의 !DumpArray를 사용할 수 있다. 출력 결과의 마지막에 보면 [0][1][2]의 3개의 요소가 NULL이 아닌 값들이 들어 있는 것을 볼 수 있다.
0:000> !DumpArray /d 02772304 Name: System.Object[] MethodTable: 5d3fab9c EEClass: 5d0bbb80 Size: 32(0x20) bytes Array: Rank 1, Number of elements 4, Type CLASS Element Methodtable: 5d44b064 [0] 0277226c [1] 0277228c [2] 027722e4 [3] null
이 3개의 요소중 첫번째 요소를 !do로 검사해 보면, 이는 MyDelegate 타입의 (SingleCast) 객체이며, _methodPtrAux가 0인 것으로 보아 Instance 메서드이고, 따라서 _target과 _methodPtr 값으로 인스턴스 메서드를 찾아낼 수 있다.
0:000> !DumpObj /d 0277226c Name: DeleApp.Program+MyDelegate MethodTable: 00143904 EEClass: 00141368 Size: 32(0x20) bytes File: C:\Dev\DeleApp\bin\Debug\DeleApp.exe Fields: MT Field Offset Type VT Attr Value Name 5d44b064 400002d 4 System.Object 0 instance 02772260 _target <== Class1 객체 5d44b064 400002e 8 System.Object 0 instance 00000000 _methodBase 5d44a0e8 400002f c System.IntPtr 1 instance 14c078 _methodPtr <== 메서드 5d44a0e8 4000030 10 System.IntPtr 1 instance 0 _methodPtrAux 5d44b064 4000031 14 System.Object 0 instance 00000000 _invocationList 5d44a0e8 4000032 18 System.IntPtr 1 instance 0 _invocationCount 0:000> !dumpmd poi(14c078+8) Method Name: DeleApp.Class1.InstRun() Class: 00141314 MethodTable: 0014385c mdToken: 06000008 Module: 00142e94 IsJitted: no CodeAddr: ffffffff Transparency: Critical
이러한 방식으로 2번째 요소가 Static 메서드를 가리킨다는 것을 알 수 있다. 그런데, 위 예제에서 3번째 메서드는 람다식을 사용했는데, 이는 어떻게 될까? 이를 동일한 방식으로 검사해 보면, <Main>b__0() 와 같이 컴파일러가 생성한 메서드명을 가리키고 있음을 알 수 있다.
0:000> !DumpObj /d 027722e4 Name: DeleApp.Program+MyDelegate MethodTable: 00143904 EEClass: 00141368 Size: 32(0x20) bytes File: C:\Dev\DeleApp\bin\Debug\DeleApp.exe Fields: MT Field Offset Type VT Attr Value Name 5d44b064 400002d 4 System.Object 0 instance 027722e4 _target 5d44b064 400002e 8 System.Object 0 instance 00000000 _methodBase 5d44a0e8 400002f c System.IntPtr 1 instance 8907ec _methodPtr 5d44a0e8 4000030 10 System.IntPtr 1 instance 14c098 _methodPtrAux <== 람다식 5d44b064 4000031 14 System.Object 0 instance 00000000 _invocationList 5d44a0e8 4000032 18 System.IntPtr 1 instance 0 _invocationCount 0:000> !dumpmd /d poi(14c098+8) Method Name: DeleApp.Program.b__0() <== 생성된 메서드 Class: 001412c0 MethodTable: 001437e8 mdToken: 06000003 Module: 00142e94 IsJitted: no CodeAddr: ffffffff Transparency: Critical
C# delegate는 해당 delegate가 SingleCast이건 MultiCast이건 호출하는 하는 방식을 동일하다. 즉, 위의 예제에서 처럼 m(); 과 같이 SingleCast 델리게이트 호출과 동일하다. 단, MultiCast 델리게이트인 경우 즉 _invocationlist가 2이상인 경우 InvocationList에서 요소를 하나씩 가져와 차례로 모두 호출한다는 차이점이 있다. 이때 만약, 한 메서드에서 오류가 발생하면 어떻게 될까? 만약 100개의 메서드가 있는데, 이 중 2번째에서 Exception이 발생하면, 나머지 98개는 실행되지 않는다. 이를 방지하려면, Delegate의 GetInvocationList()를 호출해서 손수 하나씩 처리하고, Exception이 발생하면 try.. catch로 잡아 Exception을 무시하는 것도 방법이 될 수 있다.
본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.