Microsoft MVP성태의 닷넷 이야기
.NET Framework: 819. (번역글) .NET Internals Cookbook Part 4 - Type members [링크 복사], [링크+제목 복사]
조회: 13501
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 11개 있습니다.)

(번역글) .NET Internals Cookbook Part 4 - Type members


이번에도 .NET Internals Cookbook 시리즈의 4번째 글을 번역한 것입니다.

(번역글) .NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes
; https://www.sysnet.pe.kr/2/0/11838

(번역글) .NET Internals Cookbook Part 2 - GC-related things
; https://www.sysnet.pe.kr/2/0/11869

(번역글) .NET Internals Cookbook Part 3 - Initialization tricks
; https://www.sysnet.pe.kr/2/0/11871

.NET Internals Cookbook Part 4 - Type members
; https://blog.adamfurmanek.pl/2019/03/09/net-internals-cookbook-part-4/

(번역글) .NET Internals Cookbook Part 5 - Methods, parameters, modifiers
; https://www.sysnet.pe.kr/2/0/11873

(번역글) .NET Internals Cookbook Part 6 - Object internals
; https://www.sysnet.pe.kr/2/0/11874

(번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others
; https://www.sysnet.pe.kr/2/0/11876

(번역글) .NET Internals Cookbook Part 8 - C# gotchas
; https://www.sysnet.pe.kr/2/0/11877

(번역글) .NET Internals Cookbook Part 9 - Finalizers, queues, card tables and other GC stuff
; https://www.sysnet.pe.kr/2/0/11878

(번역글) .NET Internals Cookbook Part 10 - Threads, Tasks, asynchronous code and others
; https://www.sysnet.pe.kr/2/0/11879

(번역글) .NET Internals Cookbook Part 11 - Various C# riddles
; https://www.sysnet.pe.kr/2/0/11882

(번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
; https://www.sysnet.pe.kr/2/0/11891





18. 인터페이스에 "static 생성자(type constructor)"를 추가할 수 있을까?

C#으로는 가능하지 않지만 IL 언어 수준에서는 허용이 됩니다. 예를 들어 다음과 같은 예제 코드를 만들고,

interface IFoo
{
    void DoCall();
}

class Program : IFoo
{
    static void Main(string[] args)
    {
        Program pg = new Program();
    }

    public void DoCall()
    {
    }
}

빌드한 exe 결과물을 IL 코드로 다시 풀어낸 후,

c:\temp> ildasm ConsoleApp1.exe /out=test.il

생성된 IL 파일에서 IFoo 인터페이스 정의에 직접 IL 코드로 static 생성자를 추가합니다.

...[생략]...

.class interface private abstract auto ansi IFoo
{

  .method private hidebysig specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // 
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "IFoo type constructor"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method IFoo::.cctor

  .method public hidebysig newslot abstract virtual 
          instance void  DoCall() cil managed
  {
  } // end of method IFoo::DoCall

} // end of class IFoo
...[생략]...

그다음 이것을 다시 ilasm.exe로 빌드하면 정상적인 .NET 어셈블리가 생성됩니다.

c:\temp> ilasm test.il /out=test.exe

하지만, 이런 식으로 억지로 추가된 static 생성자가 딱히 어떤 역할을 하지는 않습니다. 왜냐하면, 인터페이스 객체가 생성되는 것은 아니므로 cctor가 호출되지 않기 때문입니다.

cctor와 관련해서 또 다른 특이한 점은 구조체의 경우 기본 생성자가 호출되는 struct의 경우,

struct Vector
{
    static Vector()
    {
        Console.WriteLine("Vector cctor");
    }
}

static void Main(string[] args)
{
    Vector v = new Vector(); // static Vector.cctor가 호출되지 않음
}

static 생성자가 호출되지 않는다는 것입니다. 이는 C# 표준 문서에 명시하고 있는데,

C# Language Specification 5.0 
; https://www.microsoft.com/en-us/download/details.aspx?id=7029

11.3.10 Static constructors
    Static constructors for structs follow most of the same rules as for classes. The execution of a static constructor for a struct type is triggered by the first of the following events to occur within an application domain:
    *   A static member of the struct type is referenced.
    *   An explicitly declared constructor of the struct type is called.
    The creation of default values (§11.3.4) of struct types does not trigger the static constructor. (An example of this is the initial value of elements in an array.)

따라서 인자를 가진 생성자를 호출해야만 cctor가 호출됩니다.

struct Vector
{
    static Vector() 
    {
        Console.WriteLine("Vector cctor");
    }

    public Vector(int n)
    {
    }
}

static void Main(string[] args)
{
    Vector v = new Vector(5);
}





19. indexer 이름을 바꿀 수 있을까?

다음과 같이 .NET 1.1버전부터 추가한 IndexerName 특성을 부여해 이름 변경이 가능합니다.

using System;

public class Program
{
    public static void Main()
    {
        Foo foo = new Foo();
        Console.WriteLine(foo[1]);
    }
}

class Foo
{
    [System.Runtime.CompilerServices.IndexerName("TheItem")]
    public int this[int i]
    {
        get { return i; }
        set { } 
    }
}

이렇게 되면 빌드할 때 C# 컴파일러는 TheItem이라는 이름의 속성으로 이를 구현합니다.

.class private auto ansi beforefieldinit Foo
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Reflection.DefaultMemberAttribute::.ctor(string) = { string('TheItem') }
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
    }

    .property instance int32 TheItem
    {
        .get instance int32 Foo::get_TheItem(int32)
        .set instance void Foo::set_TheItem(int32, int32)
    }
}

이렇게 바꿀 수 있도록 한 이유에 대해 원 글(.NET Internals Cookbook Part 4 - Type members)에서는 Indexer 구문을 지원하지 않는 다른 .NET 언어를 위해 프로퍼티를 이름으로 접근할 수 있도록...이라고 하는데 제가 알기로는 그것은 틀린 설명입니다. 왜냐하면 굳이 IndexerName 특성을 사용하지 않아도 기본적으로는 "Item"이라는 이름으로 포함이 되기 때문입니다.

.property instance int32 Item
{
    .get instance int32 Foo::get_Item(int32)
    .set instance void Foo::set_Item(int32, int32)
}

이것이 문제가 되는 경우는, Item이라는 이름의 속성을 가져야 할 때 this Indexer 이름과 겹치기 때문입니다.

class Foo
{
    public int Item { get; set; }

    // 컴파일 에러
    // Error CS0102 The type 'Foo' already contains a definition for 'Item'
    public int this[int i] 
    {
        get { return i; }
        set { } 
    }
}

따라서 이럴 때 IndexerName을 이용해 이름을 다른 것으로 바꿔주면 Item 속성도 사용할 수 있고 Indexer도 사용할 수 있는 것입니다.





20. 인터페이스에 정적 필드와 메서드를 정의할 수 있을까?

이것 역시 18번에서 인터페이스의 cctor를 정의한 방법과 동일하게 IL 수준으로 처리할 수 있습니다. 따라서 다음의 C# 코드를 컴파일하고,

interface IFoo
{
}

class Program
{
    static void Main(string[] args)
    {
    }
}

ildasm.exe로 IL 코드를 구한 다음 IFoo의 코드에 다음과 같이 정적 메서드/필드를 추가할 수 있습니다.

...[생략]...

.class interface private abstract auto ansi IFoo
{
 .field private static int32 n

 .method public hidebysig static void Bar() cil managed
  {
    // 
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Bar"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method IFoo::Bar

} // end of class IFoo

...[생략]...

이를 ilasm.exe로 빌드하면 정상적인 어셈블리 파일이 생성됩니다.





21. "new Foo { Bar = 1 }" 코드는 몇 개의 객체를 생성할까요?

21번은 개인적으로 왜 이런 질문을 한 건지 이해가 안 됩니다. 다음과 같은 코드에서,

using System;

public class Program
{
    public static void Main()
    {
        Foo foo = new Foo { Bar = 5 };
    }
}

class Foo
{
    public int Bar { get; set; }
}

Foo 객체가 몇 개 생성되냐는 질문인데, 당연히 어떻게 생각해도 1개인데 왜 이런 내용을 굳이 넣었는지 모르겠습니다.

암튼, C# 3.0부터 추가된 "객체 초기화" 구문을 아느냐...는 것도 아니고 별 의미 없는 절로 보입니다.





22. 공변과 반공변이 배열과 제네릭에 어떻게 적용되는가?

이번 절은 다음의 글로 대신합니다.

C# 언어의 공변성과 반공변성
; https://www.sysnet.pe.kr/2/0/11513

자바와 닷넷의 제네릭 차이점 - 중간 언어 및 공변/반공변 처리
; https://www.sysnet.pe.kr/2/0/1581





23. dynamic 변수로 확장 메서드를 호출할 수 있을까?

dynamic이 .NET Reflection 기술의 하나이고 인스턴스에 대해 호출된다는 것을 안다면 당연히 이 대답은 "NO"임을 알 수 있을 것입니다. 설령 모르더라도 간단하게 다음의 코드로 테스트해보는 것도 가능하고.

using System;

public class Program
{
    public static void Main()
    {
        int x = 5;
        x.Extend();
        dynamic y = x;
        y.Extend(); // 실행 시 예외 발생
                    // Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'int' does not contain a definition for 'Extend'
    }
}

static class Foo
{
    public static void Extend(this int x)
    {
        Console.WriteLine(x);
    }
}

확장 메서드는 C# 코드의 관점에서 마치 객체의 멤버를 호출하는 듯이 보이지만, 내부적으로는 타입의 정적 메서드로 구현이 되기 때문에 trait과 같은 다형성을 요구하는 언어적인 특성을 구현하기에는 맞지 않습니다.

참고로, 원 글(.NET Internals Cookbook Part 4 - Type members)에서는 interface와 확장 메서드의 조합으로 동적 바인딩이 가능한 trait 패턴을 흉내 내지만 C# 8.0이 나오면 좀 더 자연스럽게 다룰 수 있게 됩니다.

C# 8: Default Interface Methods
; http://www.devsanon.com/c/c-8-default-interface-methods/





24. Equals 메서드와 == 연산자의 차이점

Equals 메서드는 System.Object의 가상 메서드로 런타임 시에 바인딩이 결정되지만, == 연산자는 컴파일 시에 (연산자 재정의를 하지 않는 한) ceq IL 코드로 번역이 됩니다. 기타 좀 더 자세한 사항은 다음의 글로 대신합니다.

== 연산자보다는 Equals 메서드의 호출이 더 권장됩니다.
; https://www.sysnet.pe.kr/2/0/2878

연산자 재정의(operator overloading)와 메서드 재정의(method overriding)의 다른 점 - 가상 함수 호출 여부
; https://www.sysnet.pe.kr/2/0/2877





25. struct에 기본 생성자(parameterless constructor)를 정의할 수 있을까?

struct에 기본 생성자 정의는 가능하지 않다고 다들 알고 있을 것입니다.

struct Vector
{
    public int x;
    public int y;

    // 컴파일 에러
    // error CS0568: Structs cannot contain explicit parameterless constructors
    public Vector() { x = 5; y = 10; }
}

하지만 이것 역시 IL 코드 상으로는 정의할 수 있습니다. 일례로 다음과 같은 코드를,

using System;

struct Vector
{
    public int x;
    public int y;
}

class Program
{
    static void Main(string[] args)
    {
        Vector v = new Vector();
        Console.WriteLine(v.x);
    }
}

ildasm.exe 처리로 생성된 IL 코드에 다음의 코드를 넣고,

...[생략]...
.class private auto ansi beforefieldinit Point
       extends [mscorlib]System.Object
{
  .field public int32 x
  .field public int32 y

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Parameterless constructor"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Foo::.ctor

} // end of class Point
...[생략]...

ilasm.exe를 이용하면 정상적인 어셈블리 생성이 됩니다. 하지만 정의만 된 것일 뿐 C# 코드의 new Vector() 코드에서 IL로 주입한 생성자가 호출되지는 않습니다. 왜냐하면 C# 컴파일러는 new Vector()를 "initobj"라는 IL 코드로 초기화하기 때문입니다. 따라서, 명시적으로 IL 코드 상에서도 생성자 호출을 하도록 다음과 같이 바꿔줘야 합니다.

.method public hidebysig static void  Main() cil managed
{
    .entrypoint
    .maxstack  1
    .locals init ([0] valuetype Foo one)
    IL_0000:  nop
    IL_0001:  ldloca.s   one
    IL_0004:  call       instance void Foo::.ctor()
    IL_0011:  ret
}

원 글(.NET Internals Cookbook Part 4 - Type members)에 보면, C# 6.0부터 구조체의 기본 생성자가 지원될 계획이었지만 Activator.CreateInstance의 버그로 누락되었다고 합니다.





26. "new Struct()"와 "default(Struct)"의 차이점이 있을까요?

일단 원 글(.NET Internals Cookbook Part 4 - Type members)에서는, new Struct는 기본 생성자를 호출하지만 default는 단순히 모든 필드를 0으로만 만든다고 하는데요. 이것이 맞는다고 하면 25번의 글에서 IL 코드에 기본 생성자 호출을 직접 추가하지 않아도 되었을 것이므로 이글 자체가 좀 원칙에 맞지 않습니다.

실제로 다음의 코드를 빌드해 보면,

struct Vector
{
}

class Program
{
    static void Main(string[] args)
    {
        Vector v1 = new Vector();
        Vector v2 = default(Vector);
    }
}

2개의 코드 모두 initobj로 초기화를 합니다.

.method private hidebysig static void  Main(string[] args) cil managed
{
    .entrypoint
    // Code size       18 (0x12)
    .maxstack  1
    .locals init ([0] valuetype Vector v1,
                [1] valuetype Vector v2)
    IL_0000:  nop
    IL_0001:  ldloca.s   v1
    IL_0003:  initobj    Vector
    IL_0009:  ldloca.s   v2
    IL_000b:  initobj    Vector
    IL_0011:  ret
} // end of method Program::Main

즉, 차이점이 없습니다. 당연하지 않을까요? 일례로 제네릭 사용 시 default 구문을 사용하게 되는데 그것이 일반적인 코드에서의 new Struct와 다르다면 문제가 발생할 여지가 있습니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 5/9/2019]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13524정성태1/12/20242063오류 유형: 890. 한국투자증권 KIS Developers OpenAPI - GW라우팅 중 오류가 발생했습니다.
13523정성태1/12/20241887오류 유형: 889. Visual Studio - error : A project with that name is already opened in the solution.
13522정성태1/11/20242019닷넷: 2200. C# - HttpClient.PostAsJsonAsync 호출 시 "Transfer-Encoding: chunked" 대신 "Content-Length" 헤더 처리
13521정성태1/11/20242082닷넷: 2199. C# - 한국투자증권 KIS Developers OpenAPI의 WebSocket Ping, Pong 처리
13520정성태1/10/20241855오류 유형: 888. C# - Unable to resolve service for type 'Microsoft.Extensions.ObjectPool.ObjectPool`....'
13519정성태1/10/20241935닷넷: 2198. C# - Reflection을 이용한 ClientWebSocket의 Ping 호출파일 다운로드1
13518정성태1/9/20242162닷넷: 2197. C# - ClientWebSocket의 Ping, Pong 처리
13517정성태1/8/20242014스크립트: 63. Python - 공개 패키지를 이용한 위성 이미지 생성 (pystac_client, odc.stac)
13516정성태1/7/20242101닷넷: 2196. IIS - AppPool의 "Disable Overlapped Recycle" 옵션의 부작용
13515정성태1/6/20242372닷넷: 2195. async 메서드 내에서 C# 7의 discard 구문 활용 사례 [1]
13514정성태1/5/20242064개발 환경 구성: 702. IIS - AppPool의 "Disable Overlapped Recycle" 옵션
13513정성태1/5/20242002닷넷: 2194. C# - WebActivatorEx / System.Web의 PreApplicationStartMethod 특성
13512정성태1/4/20241975개발 환경 구성: 701. IIS - w3wp.exe 프로세스의 ASP.NET 런타임을 항상 Warmup 모드로 유지하는 preload Enabled 설정
13511정성태1/4/20241994닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
13510정성태1/3/20241933닷넷: 2192. C# - 특정 실행 파일이 있는지 확인하는 방법 (Linux)
13509정성태1/3/20241965오류 유형: 887. .NET Core 2 이하의 프로젝트에서 System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.0.
13508정성태1/3/20241998오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/20242692닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/20232186닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20232673닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232313닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232184Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232285닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232082개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232172디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232810닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...