메모
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.
기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 그러한 차이는 언어 디자인 모임(LDM) 관련 노트에 기록됩니다.
사양문서에서 기능 사양을 C# 언어 표준으로 채택하는 과정에 대해 자세히 알아볼 수 있습니다.
챔피언 이슈: https://github.com/dotnet/csharplang/issues/39
요약
이 제안은 init 전용 속성 및 인덱서의 개념을 C#에 추가합니다.
이러한 속성과 인덱서는 개체를 생성할 때 설정할 수 있지만, 개체 생성이 완료된 후에만 효과적으로 get 상태가 됩니다.
이렇게 하면 C#에서 훨씬 더 유연하게 변경할 수 없는 모델을 사용할 수 있습니다.
동기
C#에서 변경할 수 없는 데이터를 빌드하기 위한 기본 메커니즘은 1.0 이후 변경되지 않았습니다. 그대로 남아 있습니다.
- 필드를
readonly으로 선언합니다. -
get접근자만 포함하는 속성을 선언합니다.
이러한 메커니즘은 불변 데이터를 생성하는 데 효과적이지만, 이러한 유형을 객체 및 컬렉션 초기화 기능에서 제외하고, 그 유형의 상용구 코드에 비용을 추가하는 방식으로 이를 수행합니다. 즉, 개발자는 사용 편의성과 불변성 중에서 선택해야 합니다.
Point 같은 변경할 수 없는 간단한 개체는 형식을 선언하는 것과 마찬가지로 생성을 지원하기 위해 두 배의 보일러 플레이트 코드가 필요합니다. 유형이 클수록 이 보일러 플레이트의 비용이 커지게됩니다.
struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
}
init 접근자는 호출자가 생성 작업 중에 멤버를 변경할 수 있도록 하여 변경할 수 없는 개체를 보다 유연하게 만듭니다. 즉, 개체의 변경할 수 없는 속성은 개체 이니셜라이저에 참여할 수 있으므로 형식의 모든 생성자 상용구가 필요하지 않습니다. 이제 Point 형식은 다음과 같습니다.
struct Point
{
public int X { get; init; }
public int Y { get; init; }
}
그런 다음 소비자는 개체 이니셜라이저를 사용하여 개체를 만들 수 있습니다.
var p = new Point() { X = 42, Y = 13 };
상세 디자인
init 접근자
init 전용 속성(또는 인덱서)은 init 접근자를 set 접근자 대신 사용하여 선언됩니다.
class Student
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
init 접근자를 포함하는 인스턴스 속성은 로컬 함수 또는 람다의 경우를 제외하고 다음과 같은 상황에서 설정할 수 있는 것으로 간주됩니다.
- 개체 이니셜라이저 중
-
with식 초기화 중에 - 포함하는 형식 또는 파생 형식의 인스턴스 생성자 내부의
this또는base에서 - 어떤 속성의
init접근자 내부에서,this또는base에서 - 이름이 지정된 매개 변수를 사용한 특성 사용에 대하여
init 접근자를 설정할 수 있는 위의 시간은 이 문서에서 개체의 생성 단계로 통칭됩니다.
즉, Student 클래스는 다음과 같은 방법으로 사용할 수 있습니다.
var s = new Student()
{
FirstName = "Jared",
LastName = "Parosns",
};
s.LastName = "Parsons"; // Error: LastName is not settable
init 접근자를 설정할 수 있는 경우에 대한 규칙은 형식 계층 구조 전반에 걸쳐 확장됩니다. 멤버에 액세스할 수 있고 개체가 생성 단계에 있는 것으로 알려진 경우 멤버를 설정할 수 있습니다. 이는 특히 다음을 허용합니다.
class Base
{
public bool Value { get; init; }
}
class Derived : Base
{
public Derived()
{
// Not allowed with get only properties but allowed with init
Value = true;
}
}
class Consumption
{
void Example()
{
var d = new Derived() { Value = true };
}
}
init 접근자가 호출되는 시점에서 인스턴스는 개방형 생성 단계에 있는 것으로 알려져 있습니다. 따라서 init 접근자가 일반적인 set 접근자가 수행할 수 있는 작업 외에도 다음 작업을 수행할 수 있습니다.
-
init또는this통해 사용할 수 있는 다른base접근자를 호출합니다. -
readonly통해 동일한 형식에 선언된this필드 할당
class Complex
{
readonly int Field1;
int Field2;
int Prop1 { get; init; }
int Prop2
{
get => 42;
init
{
Field1 = 13; // okay
Field2 = 13; // okay
Prop1 = 13; // okay
}
}
}
readonly 접근자에서 init 필드를 할당할 수 있는 기능은 접근자와 동일한 형식으로 선언된 필드로만 제한됩니다. 기본 형식의 readonly 필드를 할당하는 데 사용할 수 없습니다. 이 규칙은 형식 작성자가 해당 형식의 변경 가능성 동작을 계속 제어하도록 합니다.
init 활용하지 않으려는 개발자는 이를 선택하는 다른 유형의 영향을 받을 수 없습니다.
class Base
{
internal readonly int Field;
internal int Property
{
get => Field;
init => Field = value; // Okay
}
internal int OtherProperty { get; init; }
}
class Derived : Base
{
internal readonly int DerivedField;
internal int DerivedProperty
{
get => DerivedField;
init
{
DerivedField = 42; // Okay
Property = 0; // Okay
Field = 13; // Error Field is readonly
}
}
public Derived()
{
Property = 42; // Okay
Field = 13; // Error Field is readonly
}
}
가상 속성에서 init이 사용되면 모든 재정의도 init으로 표시되어야 합니다. 마찬가지로 간단한 set을(를) init로 재정의할 수 없습니다.
class Base
{
public virtual int Property { get; init; }
}
class C1 : Base
{
public override int Property { get; init; }
}
class C2 : Base
{
// Error: Property must have init to override Base.Property
public override int Property { get; set; }
}
interface 선언은 다음 패턴을 통해 init 스타일 초기화에도 참여할 수 있습니다.
interface IPerson
{
string Name { get; init; }
}
class Init
{
void M<T>() where T : IPerson, new()
{
var local = new T()
{
Name = "Jared"
};
local.Name = "Jraed"; // Error
}
}
이 기능의 제한 사항:
-
init접근자를 인스턴스 속성에만 사용할 수 있습니다. - 속성은
init및set접근자를 모두 포함할 수 없습니다. - 기본에
init이 있을 경우, 속성의 모든 재정의에init이 있어야 합니다. 이 규칙은 인터페이스 구현에도 적용됩니다.
읽기 전용 구조체
init 접근자(자동 구현 접근자와 수동으로 구현된 접근자 모두)는 readonly struct속성과 readonly 속성에서 허용됩니다.
init 접근자는 readonly 형식과 비readonlyreadonly 형식 모두에서 자신이 struct으로 표시될 수 없습니다.
readonly struct ReadonlyStruct1
{
public int Prop1 { get; init; } // Allowed
}
struct ReadonlyStruct2
{
public readonly int Prop2 { get; init; } // Allowed
public int Prop3 { get; readonly init; } // Error
}
메타데이터 인코딩
속성 init 접근자는 setmodreq로 표시된 반환 형식이 있는 표준 IsExternalInit 접근자로 내보내됩니다. 이 형식은 다음 정의를 포함하는 새 형식입니다.
namespace System.Runtime.CompilerServices
{
public sealed class IsExternalInit
{
}
}
컴파일러는 전체 이름으로 유형을 일치시킵니다. 핵심 라이브러리에 표시할 필요는 없습니다. 이 이름으로 여러 형식이 있는 경우, 컴파일러는 다음 순서로 우선순위를 결정합니다.
- 컴파일 중인 프로젝트에 정의된 것
- corelib에 정의된 것
둘 다 존재하지 않으면 형식 모호성 오류가 발생합니다.
IsExternalInit 디자인에 대한 추가 내용은 이번 호의 에 포함되어 있습니다
질문
중대한 변경 사항
이 기능을 인코딩하는 방법의 주요 피벗 지점 중 하나는 다음 질문으로 내려옵니다.
init을set로 바꾸는 것이 이진 호환성을 깨뜨리는 변경인가요?
init을 set으로 대체하여 속성을 완전히 쓰기 가능하게 만드는 것은 가상 속성이 아닌 경우 소스 호환성을 손상시키는 변경이 아닙니다. 속성을 작성할 수 있는 시나리오 집합을 확장하기만 하면 됩니다. 문제의 유일한 동작은 이진 호환성이 손상되는 변경으로 남아 있는지 여부입니다.
원본 및 이진 호환 변경에 initset 변경하려는 경우 modreqs를 솔루션으로 배제하기 때문에 아래의 modreq 및 특성 결정에 손을 대게 됩니다. 반면에 이것이 흥미롭지 않은 것으로 보일 경우, modreq와 특성 간의 결정이 덜 중요해질 것입니다.
해결 이 시나리오는 LDM에서 매력적인 것으로 간주되지 않습니다.
Modreqs와 특성
init 속성 접근자에 대한 내보내기 전략은 메타데이터를 내보낼 때 특성 또는 modreqs를 사용하는 것 중에서 선택해야 합니다. 이들은 고려해야 할 다른 절충점을 가지고 있습니다.
modreq 선언으로 속성 설정 접근자에 주석을 추가하면, CLI 준수 컴파일러는 modreq를 이해하지 않는 한 그 접근자를 무시하게 됩니다. 즉, init 인식한 컴파일러만 멤버를 읽습니다.
init 모르는 컴파일러는 set 접근자를 무시하므로 실수로 속성을 읽기/쓰기로 처리하지 않습니다.
modreq의 단점은 init이 set 접근자의 이진 서명의 일부가 된다는 것입니다.
init 추가하거나 제거하면 애플리케이션의 이진 호환성이 손상됩니다.
특성을 사용하여 set 접근자에 주석을 추가한다는 것은 특성을 이해하는 컴파일러만 해당 접근자에 대한 액세스를 제한하는 것을 알 수 있음을 의미합니다.
init 인식하지 못하는 컴파일러는 이를 간단한 읽기/쓰기 속성으로 보고 액세스를 허용합니다.
이것은 간추려 말하면 이 결정이 추가 안전을 선택하는 것과 이진 호환성을 포기하는 것 사이의 선택을 의미합니다. 조금 더 깊이 조사하면, 추가적인 안전은 보이는 것처럼 정확하지 않습니다. 다음과 같은 상황에서는 보호되지 않습니다.
-
public멤버에 대한 리플렉션 -
dynamic의 사용 - modreqs를 인식하지 못하는 컴파일러
또한 .NET 5에 대한 IL 확인 규칙을 완료하면 init 해당 규칙 중 하나가 될 것이라고 고려해야 합니다. 즉, 단순히 확인 가능한 IL을 내보내는 컴파일러를 확인하여 추가 적용을 얻을 수 있습니다.
.NET(C#, F# 및 VB)의 기본 언어는 모두 이러한 init 접근자를 인식하도록 업데이트됩니다. 따라서 여기서 유일한 현실적인 시나리오는 C# 9 컴파일러가 init 속성을 생성하고 이를 구버전 도구 집합(C# 8, VB 15 등)에서 볼 수 있는 경우입니다. 이진 호환성과의 절충안을 고려하고 평가해야 합니다.
참고 이 토론은 주로 멤버에게만 적용되며, 필드에는 적용되지 않습니다.
init 필드는 LDM에 의해 거부되었지만 modreq 및 특성 논의에서 여전히 고려할 가치가 있습니다. 필드에 대한 init 기능은 기존 readonly제한 완화입니다. 즉, 필드를 readonly + 특성으로 내보내면 이전 컴파일러가 이미 readonly인식하므로 필드를 잘못 사용할 위험이 없습니다. 따라서 여기에 modreq를 사용하면 추가 보호 기능이 추가되지 않습니다.
해결 방법 기능은 modreq를 사용하여 속성 init setter를 인코딩합니다. 설득력 있는 요인은 (특별한 순서 없이) 다음과 같습니다.
- 이전 컴파일러가
init의미 체계를 위반하지 않도록 하려는 경우 -
init선언에서virtual을 추가하거나 제거하는 것이 원본 및 이진 호환성을 모두 손상시키는 변경이 되도록 하고자 합니다.interface.
또한 init를 이진 호환성 변경으로 제거하는 데 큰 지원이 없었기 때문에 modreq를 사용하는 것이 명확한 선택이었습니다.
init 대 initonly
LDM 모임 중에 중요한 고려 사항이 있는 세 가지 구문 양식이 있었습니다.
// 1. Use init
int Option1 { get; init; }
// 2. Use init set
int Option2 { get; init set; }
// 3. Use initonly
int Option3 { get; initonly; }
해결 LDM에서 압도적으로 선호되는 구문은 없었습니다.
중요한 관심을 얻은 한 가지 점은 구문 선택이 향후 일반 기능으로 init 멤버를 수행하는 우리의 능력에 어떤 영향을 미칠 것인가하는 것이었습니다.
옵션 1을 선택하면 나중에 init 스타일 get 메서드가 있는 속성을 정의하기가 어렵습니다. 결국 향후 일반 init 멤버와 함께 나아가기로 결정할 경우, init이 속성 접근자 목록의 한정자뿐만 아니라 init set의 간단한 표시로도 사용될 수 있습니다. 기본적으로 다음 두 선언은 동일합니다.
int Property1 { get; init; }
int Property1 { get; init set; }
속성 접근자 목록에서 init을 독립 실행형 접근자로 사용하기로 결정했습니다.
초기화 실패 시 경고
다음 시나리오를 고려합니다. 형식은 생성자에 설정되지 않은 init 멤버만 선언합니다. 개체를 생성하는 코드에서 값을 초기화하지 못한 경우 경고가 표시되어야 하나요?
이 시점에서 필드가 설정되지 않을 것이 분명하므로 private 데이터를 초기화하지 못하는 것에 대한 경고와 많은 유사점이 있습니다.
따라서 경고가 여기에 아마도 유용할 것 같다.
하지만 이 경고에는 상당한 단점이 있습니다.
-
readonly에서init로 변경하는 것이 호환성 문제를 복잡하게 만듭니다. - 호출자가 초기화하는 데 필요한 멤버를 나타내려면 추가 메타데이터를 전달해야 합니다.
또한 개체 작성자가 특정 필드에 대해 경고/오류를 적용하도록 강제하는 전체 시나리오에서 여기에 가치가 있다고 생각되는 경우 이는 일반적인 기능으로 의미가 있을 수 있습니다.
init 멤버로만 제한해야 할 이유가 없습니다.
해결init 필드 및 속성 사용에 대한 경고는 없습니다.
LDM은 필요한 필드 및 속성의 개념에 대해 보다 폭넓게 설명하려고 합니다. 그러면 init 멤버 및 유효성 검사에 대한 우리의 입장을 다시 고려하게 만들 수 있습니다.
init를 필드 한정자로 허용
init 속성 접근자 역할을 할 수 있는 것과 동일한 방식으로 필드의 지정 역할을 하여 init 속성과 유사한 동작을 제공할 수도 있습니다.
형식, 파생 형식 또는 개체 이니셜라이저에 의해 생성이 완료되기 전에 필드를 할당할 수 있습니다.
class Student
{
public init string FirstName;
public init string LastName;
}
var s = new Student()
{
FirstName = "Jarde",
LastName = "Parsons",
}
s.FirstName = "Jared"; // Error FirstName is readonly
메타데이터에서 이러한 필드는 readonly 필드와 동일한 방식으로 표시되지만 추가 특성 또는 modreq를 사용하여 init 스타일 필드임을 나타냅니다.
해결책 LDM은 이 제안이 건전하다는 데 동의하지만 전반적으로 시나리오가 속성과 조화롭지 않다고 느껴졌습니다. 지금은 init 속성만 진행하기로 결정했습니다.
init 속성이 속성의 선언 형식에서 readonly 필드를 변경할 수 있으므로 적절한 수준의 유연성이 있습니다. 시나리오를 정당화하는 중요한 고객 피드백이 있는 경우 다시 고려됩니다.
init를 형식 한정자로 허용
readonly 한정자를 struct에 적용하여 모든 필드를 자동으로 readonly으로 선언할 수 있는 것처럼, init 전용 한정자는 struct나 class에 적용하여 모든 필드를 자동으로 init으로 표시할 수 있습니다.
즉, 다음 두 형식 선언은 동일합니다.
struct Point
{
public init int X;
public init int Y;
}
// vs.
init struct Point
{
public int X;
public int Y;
}
readonly struct 기능은 필드, 메서드 등 모든 멤버에 readonly 적용한다는 측면에서 간단합니다. init struct 기능은 속성에만 적용됩니다. 이것은 실제로 사용자에게 혼란을 하게 만듭니다.
init 형식의 특정 측면에 대해서만 유효하다는 점을 감안할 때 형식 한정자로 사용하는 개념을 거부했습니다.
고려 사항
호환성
기존의 init 속성과 호환되도록 get 기능은 디자인되었습니다. 특히 현재는 get만 해당되는 속성에 대해 더 유연한 객체 생성 의미 체계를 원하는 경우 완전히 가산적인 변경을 의미합니다.
예를 들어 다음 형식을 고려합니다.
class Name
{
public string First { get; }
public string Last { get; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
이러한 속성에 init을 추가하는 것은 호환성에 영향을 미치지 않는 변경입니다.
class Name
{
public string First { get; init; }
public string Last { get; init; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
IL 확인
.NET Core가 IL 확인을 다시 구현하기로 결정하면 init 멤버를 고려하여 규칙을 조정해야 합니다. 이는 readonly 데이터에 대한 비변환 액세스에 대한 규칙 변경 내용에 포함되어야 합니다.
IL 확인 규칙은 다음 두 부분으로 나누어야 합니다.
-
init멤버가readonly필드를 설정할 수 있도록 허용합니다. -
init멤버를 합법적으로 호출할 수 있는 시기를 결정합니다.
첫 번째는 기존 규칙에 대한 간단한 조정입니다. IL 검증 도구는 init 멤버를 인식하도록 가르칠 수 있으며, 그런 멤버에서 readonly에 설정할 수 있는 this 필드를 고려해야 합니다.
두 번째 규칙은 더 복잡합니다. 간단한 개체 이니셜라이저의 경우 규칙은 명확합니다.
init 식의 결과가 스택에 있는 경우 new 멤버를 호출하는 것이 합법적이어야 합니다. 값이 로컬, 배열 요소 또는 필드에 저장되거나 다른 메서드에 인수로 전달될 때까지는 여전히 init 멤버를 호출하는 것이 합법적입니다. 이렇게 하면 new 식의 결과가 명명된 식별자(this이외의)에 게시되면 더 이상 init 멤버를 호출하는 것이 더 이상 유효하지 않습니다.
더 복잡한 경우는 init 멤버, 개체 이니셜라이저 및 await을 혼합하는 경우입니다. 이것은 새로 생성된 객체가 일시적으로 상태 기계로 승격되어 필드에 배치되도록 할 수 있습니다.
var student = new Student()
{
Name = await SomeMethod()
};
여기서 new Student()의 결과는 Name이 발생하기 전에 필드로 상태 머신에 올려집니다. 컴파일러는 IL 검증 도구가 사용자가 액세스할 수 없다는 것을 이해하므로 의도한 init의미 체계를 위반하지 않는 방식으로 이러한 게양된 필드를 표시해야 합니다.
초기화 멤버
init 한정자를 확장하여 모든 인스턴스 멤버에 적용할 수 있습니다. 이렇게 하면 개체 생성 중에 init 개념이 일반화되고 형식이 생성 프로세스에 참여할 수 있는 도우미 메서드를 선언하여 init 필드 및 속성을 초기화할 수 있습니다.
이러한 멤버에는 init 접근자가 이 디자인에서 수행하는 모든 제한 사항이 있습니다. 하지만 이러한 필요성은 의심스지만 호환되는 방식으로 향후 버전의 언어에서 안전하게 추가할 수 있습니다.
세 가지 접근자 생성
init 속성의 한 가지 잠재적 구현은 initset완전히 분리하는 것입니다. 즉, 속성에는 잠재적으로 get, set및 init의 세 가지 다른 접근자가 있을 수 있습니다.
이 경우 이진 호환성을 유지하면서 modreq를 사용하여 정확성을 적용할 수 있다는 잠재적인 이점이 있습니다. 구현은 대략 다음과 같습니다.
-
init접근자는set이 있는 경우 항상 내보내집니다. 개발자가 정의하지 않은 경우 단순히set대한 참조입니다. - 개체 이니셜라이저의 속성 집합은
init이 있을 경우 항상 사용하고, 없을 경우에는 대신set을 사용합니다.
즉, 개발자는 항상 속성에서 init 안전하게 삭제할 수 있습니다.
이 디자인의 단점은 init 과거에 삭제되었는지 알 수 없으므로 init 항상 내보내야 한다고 가정해야 합니다. 이로 인해 메타데이터가 크게 확장되며, 이는 호환성의 비용을 감수할 가치가 없습니다.
C# feature specifications