성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] The Windows Registry Adventure #1: ...
[정성태] systemd for Developers I ; https:/...
[정성태] 엄밀히 object 타입의 인스턴스가 다른 타입으로 형변환 가능...
[정성태] 아래의 글에서 나오는 "Windows Application Pa...
[정성태] The history of calling conventions,...
[정성태] Secure and Deploy .NET Windows Form...
[정성태] Get Started with Milvus Vector DB i...
[정성태] cyberark/PipeViewer - A tool that...
[정성태] WinForms in a 64-Bit world – our st...
[정성태] 예제에서 SELECT_SQL도 내부적으로는 SqlCommand/...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용</h1> <p> record 정의 시, 자동으로 생성해 주는 System.Object 클래스의 메서드는 Equals, GetHashCode, ToString이 있는데요, 다시 이 중에서 사용자가 재정의할 수 있는 메서드는 ToString과 GetHashCode입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > record Vector2D(float x, float y) { public <span style='color: blue; font-weight: bold'>override</span> string ToString() { return $"2D({x},{y})"; } public <span style='color: blue; font-weight: bold'>override</span> 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(); // } } </pre> <br /> 즉, 사용자가 ToString, GetHashCode 메서드의 코드를 제공하면 C# 컴파일러는 해당 메서드에 대해서는 자동으로 코드 생성을 중복하지 않는 배려를 하는 것입니다.<br /> <br /> 이에 더해서 C# 10부터는 ToString 메서드에 한해 "sealed"도 허용하도록 바꿨습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > record Vector2D(float x, float y) { public <span style='color: blue; font-weight: bold'>sealed</span> override string ToString() { return $"2D({x},{y})"; } public override int GetHashCode() { return base.GetHashCode(); } } </pre> <br /> 그러면 해당 record를 상속받는 자식 record에서는 sealed 처리된 ToString을 인지하고 자동 생성 코드에서 ToString을 제외합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // (Vector2D에서 sealed 처리했으므로) Vector3D 클래스에는 ToString 메서드가 없음 record class Vector3D(float x, float y, float z) : <span style='color: blue; font-weight: bold'>Vector2D</span>(x, y); </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그나저나 ToString을 언제 sealed로 처리해야 할까요? 예를 들어, Book 타입이 있는데 ToString으로 isbn을 고정적으로 반환하고 싶다고 가정해보겠습니다. 그럼 다음과 같이 구현을 할 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > record Book(<span style='color: blue; font-weight: bold'>decimal isbn</span>, string title, string author) { <span style='color: blue; font-weight: bold'>public override string ToString() { return isbn.ToString(); }</span> } </pre> <br /> 그런데, 만약 이로부터 상속받은 타입을 정의하게 되면, 다시 ToString() 값의 record가 자동 생성해 주는 코드로 돌아가게 됩니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 프로그래밍", "정성태", "위키북스" ); <span style='color: blue; font-weight: bold'>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 = 위키북스 }</span> <span style='color: blue; font-weight: bold'>record Book</span>(decimal isbn, string title, string author) { public override string ToString() { return isbn.ToString(); } } <span style='color: blue; font-weight: bold'>record Ebook</span>(decimal isbn, string title, string author, string readerType) : Book(isbn, title, author); <span style='color: blue; font-weight: bold'>record Paperbook</span>(decimal isbn, string title, string author, string publisher) : Book(isbn, title, author); </pre> <br /> 그래서, 동일한 코드를 자식 클래스에서 구현해야만 하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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(); } } </pre> <br /> 만약 또 다른 자식 클래스가 정의된다면 저 (암시적) 규칙을 개발자가 알고 있지 않는 한 버그가 될 수 있습니다. 이런 불합리함을 sealed 하나로 해결할 수 있는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > record Book(decimal isbn, string title, string author) { public <span style='color: blue; font-weight: bold'>sealed</span> 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); </pre> <br /> 참고로 당연히 이것은 "record (class)"에만 유효하며, 새롭게 C# 10부터 추가된 record struct에는 적용되지 않습니다. 왜냐하면 struct의 기본 특성상 타입 자체가 상속 불가능한 sealed이기 때문입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그건 그런데, 개인적으로 왜 ToString만 허용했는지 잘 이해는 안 됩니다. 하는 김에, GetHashCode도 마저 sealed 구현이 가능하게 해주고 (재정의조차 할 수 없는) Equals도 재정의 및 sealed 구현이 가능하게 해줘야 합니다.<br /> <br /> 가령, 위의 예를 들었던 Book의 경우 Key로 동작할 수 있는 isbn 번호가 있으므로 GetHashCode를 다음과 같이 구현해도 무방합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public override int GetHashCode() { return isbn.GetHashCode(); } </pre> <br /> 하지만, ToString에서와 마찬가지로 저 코드는 자식 클래스에서도 동일하게 재정의해야 하는 문제가 발생합니다. 따라서 부모 클래스 측에서 sealed로 정의하는 것이 좋겠지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > record Book(decimal isbn, string title, string author) { // ...[생략]... // 컴파일 오류 발생 - error CS8870: 'Book.GetHashCode()' cannot be sealed because containing record is not sealed. public <span style='color: blue; font-weight: bold'>sealed</span> override int GetHashCode() { return isbn.GetHashCode(); } } </pre> <br /> 지원을 하지 않으니 현재는 불가능합니다. 재미있는 건, GetHashCode의 경우 특정 멤버만을 Key로 선정해서는 안 된다는 점입니다. 왜냐하면, Equals를 재정의할 수 없기 때문인데, record가 기본 생성해 주는 equals는 모든 멤버의 값을 비교하므로 GetHashCode로 같다고 판정해도 Equals에서 다르다고 나오는 문제가 발생합니다.<br /> <br /> 즉 (sealed를 할 수 없어 불편하지만) 다음과 같이 GetHashCode를 제공해도,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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(); } } </pre> <br /> 결국 GetHashCode의 반환과 관계없이 다음과 같이 Equals는 false를 반환합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Ebook book2 = new Ebook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "epub"); Paperbook book3 = new Paperbook(1158392478, "시작하세요. C# 9.0 프로그래밍", "정성태", "위키북스" ); Console.WriteLine(book2.GetHashCode()); // <span style='color: blue; font-weight: bold'>1158392478</span> Console.WriteLine(book3.GetHashCode()); // <span style='color: blue; font-weight: bold'>1158392478</span> <span style='color: blue; font-weight: bold'>Console.WriteLine(book2 == book3); // False</span> <span style='color: blue; font-weight: bold'>Console.WriteLine(book2.Equals(book3)); // False</span> </pre> <br /> 따라서 record의 경우 특정 멤버를 Key로 활용하고 싶어도 Dictionary 등의 자료 구조에서 원치 않는 결과가 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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); <span style='color: blue; font-weight: bold'>Console.WriteLine(books[book2]); // 출력 결과: 1 Console.WriteLine(books[book3]); // 출력 결과: 1</span> </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1851&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# 10 - (1) 구조체를 생성하는 record struct (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/record-structs'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/4334'>Static Abstract Members In Interfaces C# 10 Preview)</a> ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12790'>https://www.sysnet.pe.kr/2/0/12790</a> C# 10 - (2) 전역 네임스페이스 선언 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/globalusingdirective'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/3428'>Global Using Directive</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12792'>https://www.sysnet.pe.kr/2/0/12792</a> C# 10 - (3) 개선된 변수 초기화 판정 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/improved-definite-assignment'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/4465'>Improved Definite Assignment</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12793'>https://www.sysnet.pe.kr/2/0/12793</a> C# 10 - (4) 상수 문자열에 포맷 식 사용 가능 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/improved-interpolated-strings'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/2951'>Constant Interpolated Strings</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12796'>https://www.sysnet.pe.kr/2/0/12796</a> C# 10 - (5) 속성 패턴의 개선 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/extended-property-patterns'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/4394'>Extended property patterns</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12799'>https://www.sysnet.pe.kr/2/0/12799</a> C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용 (공식 문서, <a target='tab' href='https://github.com/dotnet/csharplang/issues/4174'>Sealed record ToString</a>) ; https://www.sysnet.pe.kr/2/0/12801 C# 10 - (7) Source Generator V2 APIs (<a target='tab' href='https://github.com/dotnet/roslyn/issues/51257'>Source Generator V2 APIs</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12804'>https://www.sysnet.pe.kr/2/0/12804</a> C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능 (공식 문서, <a target='tab' href='https://github.com/dotnet/csharplang/issues/125'>Mix declarations and variables in deconstruction</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12805'>https://www.sysnet.pe.kr/2/0/12805</a> C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/async-method-builders'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/1407'>Async method builder override</a>); ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12807'>https://www.sysnet.pe.kr/2/0/12807</a> C# 10 - (10) 개선된 #line 지시자 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/enhanced-line-directives'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/4747'>Enhanced #line directive</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12812'>https://www.sysnet.pe.kr/2/0/12812</a> C# 10 - (11) Lambda 개선 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/lambda-improvements'>공식 문서 1</a>, <a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/lambda-attributes'>공식 문서 2</a>, <a target='tab' href='https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md'>Lambda improvements</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12813'>https://www.sysnet.pe.kr/2/0/12813</a> C# 10 - (12) 문자열 보간 성능 개선 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/improved-interpolated-strings'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/4487'>Interpolated string improvements</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12826'>https://www.sysnet.pe.kr/2/0/12826</a> C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/file-scoped-namespaces'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/137'>File-scoped namespace</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12828'>https://www.sysnet.pe.kr/2/0/12828</a> C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/parameterless-struct-constructors'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/99'>Parameterless struct constructors</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12829'>https://www.sysnet.pe.kr/2/0/12829</a> C# 10 - (15) CallerArgumentExpression 특성 추가 (<a target='tab' href='https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/caller-argument-expression'>공식 문서</a>, <a target='tab' href='https://github.com/dotnet/csharplang/issues/287'>Caller expression attribute</a>) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12835'>https://www.sysnet.pe.kr/2/0/12835</a> Language Feature Status ; <a target='tab' href='https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md'>https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1636
(왼쪽의 숫자를 입력해야 합니다.)