Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 15개 있습니다.)
.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct
; https://www.sysnet.pe.kr/2/0/12790

.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언
; https://www.sysnet.pe.kr/2/0/12792

.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정
; https://www.sysnet.pe.kr/2/0/12793

.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능
; https://www.sysnet.pe.kr/2/0/12796

.NET Framework: 1100. C# 10 - (5) 속성 패턴의 개선
; https://www.sysnet.pe.kr/2/0/12799

.NET Framework: 1101. C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용
; https://www.sysnet.pe.kr/2/0/12801

.NET Framework: 1103. C# 10 - (7) Source Generator V2 APIs
; https://www.sysnet.pe.kr/2/0/12804

.NET Framework: 1104. C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능
; https://www.sysnet.pe.kr/2/0/12805

.NET Framework: 1105. C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능
; https://www.sysnet.pe.kr/2/0/12807

.NET Framework: 1108. C# 10 - (10) 개선된 #line 지시자
; https://www.sysnet.pe.kr/2/0/12812

.NET Framework: 1109. C# 10 - (11) Lambda 개선
; https://www.sysnet.pe.kr/2/0/12813

.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선
; https://www.sysnet.pe.kr/2/0/12826

.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언
; https://www.sysnet.pe.kr/2/0/12828

.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능
; https://www.sysnet.pe.kr/2/0/12829

.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가
; https://www.sysnet.pe.kr/2/0/12835




C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용

record 정의 시, 자동으로 생성해 주는 System.Object 클래스의 메서드는 Equals, GetHashCode, ToString이 있는데요, 다시 이 중에서 사용자가 재정의할 수 있는 메서드는 ToString과 GetHashCode입니다.

record Vector2D(float x, float y)
{
    public override string ToString()
    {
        return $"2D({x},{y})";
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    // 컴파일 오류 발생 - 사용자가 정의한 것을 인지하지 못하고 C# 컴파일러가 무조건 Equals 메서드를 생성하므로!
    // error CS0111: Type 'Vector2D' already defines a member called 'Equals' with the same parameter types
    // public override bool Equals(object obj)
    // {
    //     return base.Equals();
    // }
}

즉, 사용자가 ToString, GetHashCode 메서드의 코드를 제공하면 C# 컴파일러는 해당 메서드에 대해서는 자동으로 코드 생성을 중복하지 않는 배려를 하는 것입니다.

이에 더해서 C# 10부터는 ToString 메서드에 한해 "sealed"도 허용하도록 바꿨습니다.

record Vector2D(float x, float y)
{
    public sealed override string ToString()
    {
        return $"2D({x},{y})";
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

그러면 해당 record를 상속받는 자식 record에서는 sealed 처리된 ToString을 인지하고 자동 생성 코드에서 ToString을 제외합니다.

// (Vector2D에서 sealed 처리했으므로) Vector3D 클래스에는 ToString 메서드가 없음
record class Vector3D(float x, float y, float z) : Vector2D(x, y);




그나저나 ToString을 언제 sealed로 처리해야 할까요? 예를 들어, Book 타입이 있는데 ToString으로 isbn을 고정적으로 반환하고 싶다고 가정해보겠습니다. 그럼 다음과 같이 구현을 할 것입니다.

record Book(decimal isbn, string title, string author)
{
    public override string ToString()
    {
        return isbn.ToString();
    }
}

그런데, 만약 이로부터 상속받은 타입을 정의하게 되면, 다시 ToString() 값의 record가 자동 생성해 주는 코드로 돌아가게 됩니다.

Book book1 = new Book(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태");
Ebook book2 = new Ebook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "epub");
Paperbook book3 = new Paperbook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "위키북스" );

Console.WriteLine(book1); // 1158392478
Console.WriteLine(book2); // Ebook { isbn = 1158392478, title = 시작하세요. C# 9.0 프로그래밍, author = 정성태, readerType = epub }
Console.WriteLine(book3); // Paperbook { isbn = 1158392478, title = 시작하세요. C# 9.0 프로그래밍, author = 정성태, publisher = 위키북스 }

record Book(decimal isbn, string title, string author)
{
    public override string ToString()
    {
        return isbn.ToString();
    }
}

record Ebook(decimal isbn, string title, string author, string readerType) : Book(isbn, title, author);
record Paperbook(decimal isbn, string title, string author, string publisher) : Book(isbn, title, author);

그래서, 동일한 코드를 자식 클래스에서 구현해야만 하고,

record Ebook(decimal isbn, string title, string author) : Book(isbn, title, author)
{
    public override string ToString()
    {
        return isbn.ToString();
    }
}

record Paperbook(decimal isbn, string title, string author) : Book(isbn, title, author)
{
    public override string ToString()
    {
        return isbn.ToString();
    }
}

만약 또 다른 자식 클래스가 정의된다면 저 (암시적) 규칙을 개발자가 알고 있지 않는 한 버그가 될 수 있습니다. 이런 불합리함을 sealed 하나로 해결할 수 있는 것입니다.

record Book(decimal isbn, string title, string author)
{
    public sealed override string ToString()
    {
        return isbn.ToString();
    }
}

record Ebook(decimal isbn, string title, string author) : Book(isbn, title, author);

record Paperbook(decimal isbn, string title, string author) : Book(isbn, title, author);

참고로 당연히 이것은 "record (class)"에만 유효하며, 새롭게 C# 10부터 추가된 record struct에는 적용되지 않습니다. 왜냐하면 struct의 기본 특성상 타입 자체가 상속 불가능한 sealed이기 때문입니다.




그건 그런데, 개인적으로 왜 ToString만 허용했는지 잘 이해는 안 됩니다. 하는 김에, GetHashCode도 마저 sealed 구현이 가능하게 해주고 (재정의조차 할 수 없는) Equals도 재정의 및 sealed 구현이 가능하게 해줘야 합니다.

가령, 위의 예를 들었던 Book의 경우 Key로 동작할 수 있는 isbn 번호가 있으므로 GetHashCode를 다음과 같이 구현해도 무방합니다.

public override int GetHashCode()
{
    return isbn.GetHashCode();
}

하지만, ToString에서와 마찬가지로 저 코드는 자식 클래스에서도 동일하게 재정의해야 하는 문제가 발생합니다. 따라서 부모 클래스 측에서 sealed로 정의하는 것이 좋겠지만,

record Book(decimal isbn, string title, string author)
{
    // ...[생략]...

    // 컴파일 오류 발생 - error CS8870: 'Book.GetHashCode()' cannot be sealed because containing record is not sealed.
    public sealed override int GetHashCode()
    {
        return isbn.GetHashCode();
    }
}

지원을 하지 않으니 현재는 불가능합니다. 재미있는 건, GetHashCode의 경우 특정 멤버만을 Key로 선정해서는 안 된다는 점입니다. 왜냐하면, Equals를 재정의할 수 없기 때문인데, record가 기본 생성해 주는 equals는 모든 멤버의 값을 비교하므로 GetHashCode로 같다고 판정해도 Equals에서 다르다고 나오는 문제가 발생합니다.

즉 (sealed를 할 수 없어 불편하지만) 다음과 같이 GetHashCode를 제공해도,

record Book(decimal isbn, string title, string author)
{
    public override int GetHashCode()
    {
        return isbn.GetHashCode();
    }
}

record Ebook(decimal isbn, string title, string author, string readerType) : Book(isbn, title, author)
{
    public override int GetHashCode()
    {
        return isbn.GetHashCode();
    }
}

record Paperbook(decimal isbn, string title, string author, string publisher) : Book(isbn, title, author)
{
    public override int GetHashCode()
    {
        return isbn.GetHashCode();
    }
}

결국 GetHashCode의 반환과 관계없이 다음과 같이 Equals는 false를 반환합니다.

Ebook book2 = new Ebook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "epub");
Paperbook book3 = new Paperbook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "위키북스" );

Console.WriteLine(book2.GetHashCode()); // 1158392478
Console.WriteLine(book3.GetHashCode()); // 1158392478

Console.WriteLine(book2 == book3); // False
Console.WriteLine(book2.Equals(book3)); // False

따라서 record의 경우 특정 멤버를 Key로 활용하고 싶어도 Dictionary 등의 자료 구조에서 원치 않는 결과가 나옵니다.

Ebook book2 = new Ebook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "epub");
Paperbook book3 = new Paperbook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "위키북스" );

Dictionary<Book, int> books = new Dictionary<Book, int>();
books[book2] = 1;
books[book3] = (books.ContainsKey(book3) ? books[book3] + 1 : 1);
Console.WriteLine(books[book2]); // 출력 결과: 1
Console.WriteLine(books[book3]); // 출력 결과: 1

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




C# 10 - (1) 구조체를 생성하는 record struct (공식 문서, Static Abstract Members In Interfaces C# 10 Preview)
; https://www.sysnet.pe.kr/2/0/12790

C# 10 - (2) 전역 네임스페이스 선언 (공식 문서, Global Using Directive)
; https://www.sysnet.pe.kr/2/0/12792

C# 10 - (3) 개선된 변수 초기화 판정 (공식 문서, Improved Definite Assignment)
; https://www.sysnet.pe.kr/2/0/12793

C# 10 - (4) 상수 문자열에 포맷 식 사용 가능 (공식 문서, Constant Interpolated Strings)
; https://www.sysnet.pe.kr/2/0/12796

C# 10 - (5) 속성 패턴의 개선 (공식 문서, Extended property patterns)
; https://www.sysnet.pe.kr/2/0/12799

C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용 (공식 문서, Sealed record ToString)
; https://www.sysnet.pe.kr/2/0/12801

C# 10 - (7) Source Generator V2 APIs (Source Generator V2 APIs)
; https://www.sysnet.pe.kr/2/0/12804

C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능 (공식 문서, Mix declarations and variables in deconstruction)
; https://www.sysnet.pe.kr/2/0/12805

C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능 (공식 문서, Async method builder override); 
; https://www.sysnet.pe.kr/2/0/12807

C# 10 - (10) 개선된 #line 지시자 (공식 문서, Enhanced #line directive)
; https://www.sysnet.pe.kr/2/0/12812

C# 10 - (11) Lambda 개선 (공식 문서 1, 공식 문서 2, Lambda improvements) 
; https://www.sysnet.pe.kr/2/0/12813

C# 10 - (12) 문자열 보간 성능 개선 (공식 문서, Interpolated string improvements)
; https://www.sysnet.pe.kr/2/0/12826

C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언 (공식 문서, File-scoped namespace)
; https://www.sysnet.pe.kr/2/0/12828

C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능 (공식 문서, Parameterless struct constructors)
; https://www.sysnet.pe.kr/2/0/12829

C# 10 - (15) CallerArgumentExpression 특성 추가 (공식 문서, Caller expression attribute)
; https://www.sysnet.pe.kr/2/0/12835

Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md




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







[최초 등록일: ]
[최종 수정일: 7/14/2022]

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

비밀번호

댓글 작성자
 




... 121  122  [123]  124  125  126  127  128  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
10848정성태9/8/201527458VS.NET IDE: 103. Visual Studio의 Ctrl + F5 실행 동작파일 다운로드1
10847정성태9/8/201523502VS.NET IDE: 102. 목록(List) 타입의 값을 디버깅 중 Watch 창에서 확인하는 방법 [1]파일 다운로드1
10846정성태9/8/201520881오류 유형: 306. "query user" 명령어에 공백 문자가 포함된 계정을 전달하는 경우
10845정성태9/3/201521978오류 유형: 305. 윈도우 백업 시 오류 - 0x80780166
10844정성태9/2/201523178.NET Framework: 528. C# - 상호 참조하는 경우의 정적 생성자 동작 방식 [4]파일 다운로드1
10843정성태9/1/201523945VS.NET IDE: 101. Visual Studio 2015의 솔루션 탐색기가 클래스 뷰 정보로 인해 느려지는 현상
10842정성태9/1/201520935.NET Framework: 527. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 세 번째 이야기
10841정성태8/31/201530269개발 환경 구성: 276. Visual Studio 2013에서 C# 6과 닷넷 4.6 기능을 사용하려면?
10839정성태8/22/201528820Windows: 112. 윈도우 10에서 터치 키보드를 안 뜨게 할 수 있는 방법 [4]
10838정성태8/22/201539110오류 유형: 304. Windows 10에서 VPN 연결이 실패한다면? [3]
10837정성태8/21/201519394오류 유형: 303. Your computer is low on memory. Save your files and close these programs...
10836정성태8/21/201520363오류 유형: 302. 설치 파일 실행 시 "This app can't run on your PC" 오류가 뜬다면?
10835정성태8/21/201528847웹: 31. Microsoft Edge 브라우저를 명령행에서 띄우는 방법 [1]
10834정성태8/19/201520983.NET Framework: 526. 닷넷 - 값 형식을 new 없이 생성하면 0으로 초기화되지 않는다?
10833정성태8/18/201525630.NET Framework: 525. C# - 닷넷에서 프로세스가 열고 있는 파일 목록을 구하는 방법파일 다운로드1
10832정성태8/17/201530009디버깅 기술: 74. x64 콜 스택 인자 추적과 windbg의 Child-SP, RetAddr, Args to Child 값 확인 [8]파일 다운로드2
10831정성태8/13/201529975.NET Framework: 524. .NET 4.0과 .NET 4.5의 컴파일 결과 차이점 [1]파일 다운로드1
10830정성태8/12/201524024개발 환경 구성: 275. Web.config이 적용되지 않는 프로젝트에서 Razor 템플릿 파일의 C# 컴파일러 버전 제어 [1]
10829정성태8/10/201526125개발 환경 구성: 274. PowerShell/명령행에서 JDK/JRE를 무인(unattended)/자동 설치를 하는 방법 [3]
10828정성태8/10/201531699웹: 30. Edge 브라우저에서 "이 웹 사이트에는 Internet Explorer가 필요함" 단계를 없애는 방법 [1]
10827정성태7/8/201533036개발 환경 구성: 273. Visual Studio 2015에서 Github와 연동하는 방법 [3]
10826정성태7/8/201522669오류 유형: 301. The trust relationship between this workstation and the primary domain failed. - 두 번째 이야기
10825정성태7/8/201522246개발 환경 구성: 272. Visual Studio IDE 설치 없이 Visual Studio SDK 설치하는 방법
10824정성태7/7/201527437개발 환경 구성: 271. Team Foundation Server 2015 설치 방법 [1]
10823정성태7/7/201527940오류 유형: 300. SqlException (0x80131904): Unable to open the physical file
10822정성태7/7/201527284오류 유형: 299. The 'Visual C++ Project System Package' package did not load correctly.
... 121  122  [123]  124  125  126  127  128  129  130  131  132  133  134  135  ...