다음을 통해 공유


기본 생성자

메모

이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.

기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 언어 디자인 모임(LDM) 관련 노트에서 캡처됩니다.

사양문서에서 기능 사양 문서를 C# 언어 표준으로 채택하는 프로세스에 대해 자세히 알아볼 수 있습니다.

요약

클래스 및 구조체에는 매개 변수 목록이 있을 수 있으며 해당 기본 클래스 사양에는 인수 목록이 있을 수 있습니다. 기본 생성자 매개 변수는 클래스 또는 구조체 선언 전체의 범위에 있으며 함수 멤버 또는 익명 함수에 의해 캡처되는 경우 적절하게 저장됩니다(예: 선언된 클래스 또는 구조체의 말할 수 없는 프라이빗 필드).

제안은 여러 추가 멤버를 합성하여, "retcons"라는 방식으로 더 일반적인 기능 내에서 레코드의 기본 생성자를 재구성하려고 합니다.

동기

C#의 클래스 또는 구조체가 둘 이상의 생성자를 가질 수 있는 기능은 일반성을 제공하지만 생성자 입력과 클래스 상태를 완전히 구분해야 하므로 선언 구문에서 일부 지루함을 희생합니다.

기본 생성자는 초기화 또는 개체 상태로 직접 사용할 전체 클래스 또는 구조체의 범위에 한 생성자의 매개 변수를 배치합니다. 단점은 다른 생성자가 주 생성자를 통해 호출해야 한다는 것입니다.

public class B(bool b) { } // base class

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(S));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

상세 디자인

레코드 및 비 레코드에서 일반화된 디자인을 설명한 다음, 기본 생성자가 있는 상태에서 합성된 멤버 집합을 추가하여 레코드에 대한 기존 기본 생성자를 지정하는 방법을 자세히 설명합니다.

통사론

클래스 및 구조체 선언은 형식 이름에 대한 매개변수 목록, 기본 클래스에 대한 인수 목록, 그리고 오직 ;으로만 구성된 본문을 허용하도록 확장됩니다.

class_declaration
  : attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
  parameter_list? class_base? type_parameter_constraints_clause* class_body
  ;
  
class_designator
  : 'record' 'class'?
  | 'class'
  
class_base
  : ':' class_type argument_list?
  | ':' interface_type_list
  | ':' class_type  argument_list? ',' interface_type_list
  ;  
  
class_body
  : '{' class_member_declaration* '}' ';'?
  | ';'
  ;
  
struct_declaration
  : attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
    parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
  ;

struct_body
  : '{' struct_member_declaration* '}' ';'?
  | ';'
  ;
  
interface_declaration
  : attributes? interface_modifier* 'partial'? 'interface'
    identifier variant_type_parameter_list? interface_base?
    type_parameter_constraints_clause* interface_body
  ;  
    
interface_body
  : '{' interface_member_declaration* '}' ';'?
  | ';'
  ;

enum_declaration
  : attributes? enum_modifier* 'enum' identifier enum_base? enum_body
  ;

enum_body
  : '{' enum_member_declarations? '}' ';'?
  | '{' enum_member_declarations ',' '}' ';'?
  | ';'
  ;

참고: 이러한 프로덕션은 레코드record_declaration 대체하고 레코드 구조체record_struct_declaration 모두 사용되지 않습니다.

외부 class_declarationparameter_list가 포함되어 있지 않을 경우, class_baseargument_list이 있는 것은 오류입니다. 부분 클래스 또는 구조체의 하나 이상의 부분 형식 선언은 parameter_list제공할 수 있습니다. record 선언의 parameter_list 매개 변수는 모두 값 매개 변수여야 합니다.

참고로, 이 제안에 따르면 class_body, struct_body, interface_bodyenum_body는 오직 ;로만 구성될 수 있습니다.

parameter_list이 있는 클래스 또는 구조체에는 형식 선언의 값 매개 변수에 해당하는 시그니처를 가진 암시적 공용 생성자가 있습니다. 이는 이 형식의 기본 생성자라고 하며, 암시적으로 선언된 매개변수 없는 생성자(있는 경우)를 억제합니다. 형식 선언에 이미 동일한 서명이 있는 기본 생성자와 생성자가 있는 것은 오류입니다.

조회

간단한 이름의 조회가 기본 생성자 매개 변수를 처리할 수 있도록 강화됩니다. 변경 내용은 다음 발췌문에서 굵게 강조 표시됩니다.

  • 그렇지 않으면 각 인스턴스 형식 T(§15.3.2)에 대해 즉시 바깥쪽 형식 선언의 인스턴스 형식으로 시작하고 각 바깥쪽 클래스 또는 구조체 선언의 인스턴스 형식(있는 경우)을 계속합니다.
    • T 선언에 기본 생성자 매개 변수 I 이 포함되어 있고, 참조가 Tclass_baseargument_list 내에서 발생하거나 T의 필드, 프로퍼티 또는 이벤트의 초기화에서 발생하는 경우 결과는 기본 생성자 매개 변수 I 입니다.
    • e 0이고 T 선언에 이름 I있는 형식 매개 변수가 포함되어 있으면simple_name 해당 형식 매개 변수를 참조합니다.
    • 그렇지 않으면 TI 멤버 조회(§12.5)에서 e 형식 인수가 일치를 생성하는 경우:
      • 즉시 바깥 클래스 또는 구조체 유형의 인스턴스 유형인 T의 경우, 조회가 하나 이상의 메서드를 식별하면 결과는 this의 인스턴스 식과 연결된 메서드 그룹입니다. 형식 인수 목록을 지정한 경우 제네릭 메서드(§12.8.10.2)를 호출하는 데 사용됩니다.
      • 그 외에, 만약 T가 접근하고자 하는 클래스 또는 구조체의 인스턴스 형식이고, 조회에서 인스턴스 멤버를 식별하며, 그 참조가 인스턴스 생성자, 인스턴스 메서드, 또는 인스턴스 접근자(§12.2.1)의 블록 내에서 발생한다면, 결과는 this.I멤버 액세스(§12.8.7)와 같습니다. 이 문제는 e 0일 때만 발생할 수 있습니다.
      • 그렇지 않으면 결과는 T.I 또는 T.I<A₁, ..., Aₑ>형태의 멤버 접근(§12.8.7)과 동일합니다.
    • 그렇지 않으면 T 선언에 기본 생성자 매개 변수 I가 포함되어 있는 경우, 결과는 기본 생성자 매개 변수 I입니다.

첫 번째 추가는레코드에서 기본 생성자에 의해 발생하는 변경 내용에 해당하며, 이니셜라이저 및 기본 클래스 인수 내의 해당 필드 앞에 기본 생성자 매개 변수가 있는지 확인합니다. 이 규칙도 정적 이니셜라이저로 확장합니다. 그러나 레코드에는 항상 매개 변수와 이름이 같은 인스턴스 멤버가 있으므로 확장은 오류 메시지의 변경으로 이어질 수 있습니다. 매개 변수에 대한 불법적인 접근과 인스턴스 멤버에 대한 불법적인 접근입니다.

두 번째 추가를 사용하면 기본 생성자 매개 변수가 형식 본문 내의 다른 위치에서도 나타날 수 있지만, 이는 멤버에 의해 가려지지 않는 경우에만 가능합니다.

다음 중 하나 내에서 참조가 발생하지 않는 경우 기본 생성자 매개 변수를 참조하는 것은 오류입니다.

  • nameof 인수
  • 선언 형식의 인스턴스 필드, 속성 또는 이벤트(매개 변수를 사용하여 기본 생성자를 선언하는 형식)의 이니셜라이저입니다.
  • 선언 형식의 argument_listclass_base.
  • 선언 형식의 인스턴스 메서드 본문(인스턴스 생성자는 제외됨)입니다.
  • 선언 형식의 인스턴스 접근자의 본문입니다.

즉, 기본 생성자 매개 변수는 선언 형식 본문 전체의 범위에 있습니다. 선언 형식의 이니셜라이저, 선언 형식의 속성 또는 이벤트 내 또는 선언 형식의 class_baseargument_list 내에서 선언 형식의 멤버를 숨깁니다. 그 외의 모든 곳에서, 명시 타입의 멤버에 의해 가려집니다.

따라서 다음 선언에서:

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

필드 i 이니셜라이저는 매개 변수 i참조하는 반면 속성 I 본문은 필드 i참조합니다.

기본 멤버에 의해 숨겨진 것에 대한 경고

기본 생성자 매개 변수가 생성자를 통해 기본 형식에 전달되지 않은 경우 기본 멤버가 기본 생성자 매개 변수를 숨기는 경우 컴파일러에서 식별자 사용에 대한 경고를 생성합니다.

기본 생성자 매개 변수는 class_base인수에 대해 다음 조건이 모두 true인 경우 생성자를 통해 기본 형식에 전달되는 것으로 간주됩니다.

  • 인수는 기본 생성자 매개 변수의 암시적 또는 명시적 ID 변환을 나타냅니다.
  • 인수는 확장된 params 인수의 일부가 아닙니다.

의미론

기본 생성자는 지정된 매개 변수를 사용하여 포함하는 형식에 인스턴스 생성자를 생성합니다. class_base 인수 목록이 있는 경우 생성된 인스턴스 생성자에 동일한 인수 목록이 있는 base 이니셜라이저가 있습니다.

클래스/구조체 선언의 기본 생성자 매개 변수는 ref, in 또는 out선언할 수 있습니다. ref 또는 out 매개 변수를 선언하는 것은 레코드 선언의 기본 생성자에서 잘못된 상태로 유지됩니다.

클래스 본문의 모든 인스턴스 멤버 이니셜라이저는 생성된 생성자의 할당이 됩니다.

기본 생성자 매개 변수가 인스턴스 멤버 내부에서 참조되고, 그 참조가 nameof 인수 내부에 있지 않은 경우, 생성자가 종료된 후에도 액세스할 수 있도록, 해당 매개 변수는 외부 형식의 상태로 캡처됩니다. 유력한 구현 전략은 난독화된 이름을 사용하는 프라이빗 필드를 통해 이루어질 수 있습니다. 읽기 전용 구조체에서 캡처 필드는 읽기 전용으로 표시됩니다. 따라서 읽기 전용 구조체의 캡처된 매개 변수에 대한 액세스는 읽기 전용 필드에 대한 액세스와 유사한 제한을 가합니다. 읽기 전용 멤버 내에서 캡처된 매개 변수에 대한 액세스는 동일한 컨텍스트의 인스턴스 필드에 대한 액세스와 유사한 제한을 가합니다.

ref와 유사한 형식의 매개 변수에는 캡처가 허용되지 않으며 ref, in 또는 out 매개 변수에는 캡처가 허용되지 않습니다. 이는 람다에서 캡처하기 위한 제한 사항과 유사합니다.

기본 생성자 매개 변수가 인스턴스 멤버 이니셜라이저 내에서만 참조되는 경우, 이 이니셜라이저는 생성된 생성자의 매개 변수를 직접 참조할 수 있으며, 이러한 이니셜라이저는 생성자의 일부로 실행됩니다.

기본 생성자는 다음 작업 시퀀스를 수행합니다.

  1. 매개 변수 값은 캡처 필드에 저장됩니다(있는 경우).
  2. 인스턴스 이니셜라이저가 실행됩니다.
  3. 기본 생성자 이니셜라이저가 호출됩니다.

모든 사용자 코드의 매개 변수 참조는 해당 캡처 필드 참조로 바뀝니다.

예를 들어 이 선언은 다음과 같습니다.

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

다음과 유사한 코드를 생성합니다.

public class C : B
{
    public int I { get; set; }
    public string S
    {
        get => __s;
        set => __s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(0, s) { ... } // must call this(...)
    
    // generated members
    private string __s; // for capture of s
    public C(bool b, int i, string s)
    {
        __s = s; // capture s
        I = i; // run I's initializer
        B(b) // run B's constructor
    }
}

기본이 아닌 생성자 선언이 기본 생성자와 동일한 매개 변수 목록을 갖는 것은 오류입니다. 기본이 아닌 모든 생성자 선언은 this 이니셜라이저를 사용해야 하므로 주 생성자가 궁극적으로 호출됩니다.

주 생성자 매개 변수가 (자동으로 생성될 수 있는) 인스턴스 이니셜라이저 또는 기본 이니셜라이저 내에서 읽히지 않을 경우, 레코드에서 경고를 생성합니다. 클래스 및 구조체의 기본 생성자 매개 변수에 대해 유사한 경고가 보고됩니다.

  • 값별 매개 변수의 경우, 매개 변수가 캡처되지 않고 인스턴스 이니셜라이저 또는 기본 이니셜라이저 내에서 읽히지 않는다면.
  • 인스턴스 이니셜라이저나 기본 이니셜라이저 내에서 매개 변수가 읽히지 않은 경우, in 매개 변수를 위한 것입니다.
  • ref 매개 변수가 인스턴스 이니셜라이저나 기본 이니셜라이저 내에서 읽히거나 쓰이지 않는 경우에 대해.

동일한 기본 이름 및 형식 이름

"색 색" 시나리오라고도 하는 경우에는 동일한 단순 이름과 형식 이름에 대한 특수 언어 규칙이 있습니다.

양식 E.I의 멤버 접근에서, E이 단일 식별자이고 Esimple_name (§12.8.4)의 의미로 상수, 필드, 속성, 지역 변수 또는 매개 변수이며, 이러한 요소들이 Etype_name (§7.8.1)으로 해석될 때와 동일한 유형인 경우, E의 두 가지 가능한 의미가 모두 허용됩니다. E.I의 멤버 조회는 결코 모호하지 않습니다. 이는 I이 두 경우 모두 E의 멤버여야 하기 때문입니다. 즉, 이 규칙은 단순히 컴파일 시간 오류가 발생할 수 있었던 상황에서 E의 정적 멤버와 중첩된 형식에 대한 액세스를 허용합니다.

기본 생성자와 관련하여 규칙은 인스턴스 멤버 내의 식별자를 형식 참조로 처리할지 또는 기본 생성자 매개 변수 참조로 처리해야 하는지에 영향을 줍니다. 그러면 매개 변수를 바깥쪽 형식의 상태로 캡처합니다. "E.I의 멤버 조회는 결코 모호하지 않지만, 조회 결과가 멤버 그룹일 때 멤버 액세스를 완전히 확인(바인딩)하지 않고는 멤버 액세스가 정적 멤버를 참조하는지 인스턴스 멤버를 참조하는지 여부를 확인할 수 없는 경우도 있습니다." 동시에 기본 생성자 매개 변수를 캡처하면 포함하는 유형의 속성이 변경되어 의미 분석에 영향을 줍니다. 예를 들어 형식이 관리되지 않아서 특정 제약 조건이 실패할 수 있습니다. 매개 변수가 캡처된 것으로 간주되는지 여부에 따라 어느 쪽이든 바인딩이 성공할 수 있는 시나리오도 있습니다. 예를 들어:

struct S1(Color Color)
{
    public void Test()
    {
        Color.M1(this); // Error: ambiguity between parameter and typename
    }
}

class Color
{
    public void M1<T>(T x, int y = 0)
    {
        System.Console.WriteLine("instance");
    }
    
    public static void M1<T>(T x) where T : unmanaged
    {
        System.Console.WriteLine("static");
    }
}

수신기 Color를 값으로 간주하면 매개 변수를 포착하게 되고 'S1'이 제어됩니다. 그런 다음 제약 조건으로 인해 정적 메서드를 적용할 수 없게 되고 인스턴스 메서드를 호출합니다. 그러나 수신기를 형식으로 취급하는 경우 매개 변수를 캡처하지 않고 'S1'은 관리되지 않는 상태로 유지됩니다. 두 메서드 모두 적용할 수 있지만 선택적 매개 변수가 없기 때문에 정적 메서드는 "더 낫습니다". 두 선택 모두 오류로 이어지지 않지만 각각 고유한 동작이 발생합니다.

이 경우 컴파일러는 다음 조건이 모두 충족되면 멤버 액세스 E.I 대한 모호성 오류를 생성합니다.

  • E.I 멤버 조회는 인스턴스와 정적 멤버를 포함하는 멤버 그룹을 동시에 생성합니다. 수신기 형식에 적용할 수 있는 확장 메서드는 이 검사를 위해 인스턴스 메서드로 처리됩니다.
  • E이 형식 이름이 아닌 단순 이름으로 처리된다면, 주 생성자 매개 변수를 참조하며 이 매개 변수를 포함된 유형의 상태로 캡처하게 됩니다.

이중 스토리지 경고

기본 생성자 매개 변수가 기반에 전달되고 이 캡처되는 경우, 실수로 객체에 두 번 저장될 위험이 높습니다.

컴파일러는 다음의 모든 조건이 충족될 때, class_baseargument_list 내의 in 또는 값 인수에 대해 경고를 생성합니다.

  • 인수는 기본 생성자 매개 변수의 암시적 또는 명시적 ID 변환을 나타냅니다.
  • 인수는 확장된 params 인수의 일부가 아닙니다.
  • 기본 생성자 매개 변수는 포함하는 타입의 상태로 캡처됩니다.

컴파일러는 다음 조건이 모두 true이면 variable_initializer 대한 경고를 생성합니다.

  • 변수 이니셜라이저는 기본 생성자 매개 변수의 암시적 또는 명시적 ID 변환을 나타냅니다.
  • 주 생성자 매개 변수는 포함하는 형식의 상태로 캡처됩니다.

예를 들어:

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

기본 생성자를 대상으로 하는 특성

https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md에 우리는 https://github.com/dotnet/csharplang/issues/7047 제안을 받아들이기로 결정했습니다.

"method" 특성은 class_declaration/ struct_declaration에 parameter_list가 포함된 경우 허용되며, 그 결과 해당 특성을 가진 기본 생성자가 생성됩니다. class_declaration/struct_declarationparameter_list이 없는 경우 method 대상을 가진 특성은 경고와 함께 무시됩니다.

[method: FooAttr] // Good
public partial record Rec(
    [property: Foo] int X,
    [field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
    public void Frobnicate()
    {
        ...
    }
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;

레코드의 기본 생성자

이 제안을 사용하면 레코드가 더 이상 기본 생성자 메커니즘을 별도로 지정할 필요가 없습니다. 대신 기본 생성자가 있는 레코드(클래스 및 구조체) 선언은 다음과 같은 간단한 추가와 함께 일반 규칙을 따릅니다.

  • 각 기본 생성자 매개 변수에 대해 이름이 같은 멤버가 이미 있는 경우 인스턴스 속성 또는 필드여야 합니다. 그렇지 않은 경우, 매개변수로부터 할당하는 속성 이니셜라이저와 함께 동일한 이름의 public 초기화 전용 자동 속성이 생성됩니다.
  • 분해기는 기본 생성자 매개 변수와 일치하도록 out 매개 변수와 함께 구성됩니다.
  • 명시적 생성자 선언이 자신이 속한 형식의 단일 매개변수를 받는 "복사 생성자"인 경우, this 이니셜라이저를 호출할 필요가 없으며, 레코드 선언에 포함된 멤버 이니셜라이저를 실행하지 않습니다.

단점

  • 컴파일러가 클래스의 전체 텍스트를 기반으로 기본 생성자 매개 변수에 대한 필드를 할당할지 여부를 결정하므로 생성된 개체의 할당 크기는 명확하지 않습니다. 이 위험은 람다 식에 의한 변수의 암시적 캡처와 유사합니다.
  • 일반적인 유혹(또는 우발적 패턴)은 기본 클래스에서 보호된 필드를 명시적으로 할당하는 대신, 생성자의 체인을 통해 여러 상속 수준에서 "동일한" 매개 변수를 캡처함으로써 개체들 내에서 동일한 데이터에 대한 중복 할당을 초래하는 것일 수 있습니다. 이는 자동 속성을 사용하여 자동 속성을 재정의하는 오늘날의 위험과 매우 유사합니다.
  • 여기서 제안한 대로, 생성자 본문에 일반적으로 포함될 수 있는 추가 논리를 넣을 자리가 없습니다. 아래의 "기본 생성자 본문" 확장은 그것을 해결합니다.
  • 제안된 대로 실행 순서 의미 체계는 일반 생성자 내에서 미묘하게 다르며 멤버 이니셜라이저를 기본 호출 후로 지연합니다. 이것은 아마도 해결할 수 있지만 일부 확장 제안들(특히 "기본 생성자 본문")을 희생해야 할 수도 있습니다.
  • 이 제안은 단일 생성자를 기본으로 지정할 수 있는 시나리오에서만 작동합니다.
  • 클래스와 기본 생성자의 별도의 접근성을 표현할 수 있는 방법은 없습니다. 예를 들어 공용 생성자가 모두 하나의 프라이빗 "build-it-all" 생성자에 위임하는 경우입니다. 필요한 경우 나중에 해당 구문을 제안할 수 있습니다.

대안

캡처 없음

이 기능의 훨씬 간단한 버전은 멤버 본문에서 기본 생성자 매개 변수가 발생하지 않도록 합니다. 참조하는 것은 오류입니다. 스토리지가 초기화 코드 이상으로 필요한 경우 필드를 명시적으로 선언해야 합니다.

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

이것은 나중에 전체 제안으로 발전할 수 있고, 초기에 상용구를 덜 제거하는 비용을 치르더라도 여러 결정과 복잡성을 피할 수 있으며, 아마 직관적이지 않게 보이겠지만.

명시적 생성된 필드

다른 방법은 기본 생성자 매개 변수가 항상 동일한 이름의 필드를 항상 눈에 띄게 생성하는 것입니다. 로컬 및 익명 함수와 동일한 방식으로 매개 변수를 닫는 대신 레코드의 기본 construcor 매개 변수에 대해 생성된 공용 속성과 유사하게 생성된 멤버 선언이 명시적으로 존재합니다. 레코드와 마찬가지로 적절한 멤버가 이미 있는 경우 생성되지 않습니다.

생성된 필드가 private이면 멤버 본문 내에서 필드로 사용되지 않을 경우 계속 생략될 수 있습니다. 그러나 클래스에서 프라이빗 필드는 파생 클래스에서 발생할 수 있는 상태 중복으로 인해 적절한 선택이 아닌 경우가 많습니다. 여기서 옵션은 대신 클래스에서 보호된 필드를 생성하여 상속 계층 간에 스토리지를 다시 사용하도록 장려하는 것입니다. 그러나 선언을 취소할 수 없으며 모든 기본 생성자 매개 변수에 대한 할당 비용이 발생합니다.

이렇게 하면 레코드가 아닌 기본 생성자를 레코드 생성자와 더 긴밀하게 정렬합니다. 즉, 멤버는 항상(적어도 개념적으로) 생성되지만 액세스 권한이 다른 멤버의 종류는 다릅니다. 그러나 C#의 다른 부분에서 매개 변수와 지역 변수를 캡처하는 방법과는 놀라운 차이점이 있게 됩니다. 로컬 클래스를 허용하게 된다면, 예를 들어, 그것들은 암시적으로 둘러싸고 있는 매개변수와 지역 변수를 캡처하게 될 것입니다. 그들에게 섀도잉 필드를 눈에 띄게 만드는 것은 합리적인 행동이 아닐 것 같습니다.

이 접근 방식에서 자주 발생하는 또 다른 문제는 많은 개발자가 매개 변수 및 필드에 대해 서로 다른 명명 규칙을 가지고 있다는 것입니다. 기본 생성자 매개 변수에 사용해야 하는 항목은 무엇입니까? 둘 중 하나를 선택하면 코드의 나머지 부분과 불일치가 발생합니다.

마지막으로, 멤버 선언을 눈에 띄게 생성하는 것이 레코드에는 중요한 역할을 하지만, 레코드가 아닌 클래스와 구조체에서는 훨씬 더 놀랍고 기대와 다릅니다. 결론적으로, 이러한 이유들로 인해 주 제안이 암시적 캡처를 선택하게 되며, 원하는 경우 명시적 멤버 선언에 대해 레코드와 일치하는 합리적인 동작을 제공합니다.

이니셜라이저 범위에서 인스턴스 멤버 제거

위의 조회 규칙은 해당 멤버가 수동으로 선언될 때 레코드에서 기본 생성자 매개 변수의 현재 동작을 허용하고 생성되지 않은 경우 생성된 멤버의 동작을 설명하기 위한 것입니다. 참조가 발생하는 위치에 따라 "초기화 범위"(this/base 이니셜라이저, 멤버 이니셜라이저)와 "본문 범위"(멤버 본문)를 구분하는 조사가 필요합니다. 이는 기본 생성자 매개 변수를 찾을 때 를 변경하여 위의 제안으로 달성됩니다.

이니셜라이저 범위에서 간단한 이름으로 인스턴스 멤버를 참조하면 항상 오류가 발생한다는 관측이 있습니다. 단순히 해당 위치에서 인스턴스 멤버를 숨기지 말고, 범위에서 벗어나게 할 수 있을까요? 범위의 이상한 조건부 순서가 없게 될 것입니다.

이 대안은 아마도 가능하지만, 다소 광범위하고 잠재적으로 바람직하지 않은 몇 가지 결과가 있을 것입니다. 우선 이니셜라이저 범위에서 인스턴스 멤버를 제거하면 수행하는 간단한 이름이 인스턴스 멤버에 해당하며 기본 생성자 매개 변수에 않는 형식 선언 외부의 항목에 실수로 바인딩할 수 있습니다. 이 경우 의도적인 경우는 거의 없을 것이며, 오류가 발생하는 것이 더 나을 것입니다.

또한 정적 멤버는 초기화 범위에서 참조하는 것이 좋습니다. 따라서 조회에서 정적 멤버와 인스턴스 멤버를 구분해야 하며, 현재는 그렇지 않습니다. (우리는 오버로드 해결에서 구분할 수 있지만, 여기서는 해당되지 않습니다.) 따라서 변경해야 하므로 정적 컨텍스트에서 인스턴스 멤버를 찾았기 때문에 오류가 아닌 "더 멀리" 바인딩되는 상황이 더 많이 발생합니다.

결국, 이 "단순화"는 아무도 원치 않는 예기치 않은 복잡한 문제로 이어질 것입니다.

가능한 확장

이는 핵심 제안과 함께 고려될 수 있는 변형 또는 추가 사항이며, 유용하다고 판단되는 경우 이후 단계에서 고려할 수 있습니다.

기본 생성자의 매개 변수를 생성자 내에서 액세스하기

위의 규칙에 따르면 다른 생성자 내에서 주 생성자 매개 변수를 참조하는 것은 오류로 간주됩니다. 그러나 기본 생성자가 먼저 실행되기 때문에 다른 생성자의 본문 내에서 허용될 수 있습니다. 그러나 this 이니셜라이저의 인수 목록 내에서 허용되지 않는 상태를 유지해야 합니다.

public class C(bool b, int i, string s) : B(b)
{
    public C(string s) : this(b, s) // b still disallowed
    { 
        i++; // could be allowed
    }
}

기본 생성자가 이미 실행된 후에는 생성자 본문이 변수를 사용하기 위한 유일한 방법이 캡처이기 때문에, 이러한 액세스에는 여전히 캡처가 발생하게 됩니다.

이 이니셜라이저의 인수에서 기본 생성자 매개 변수를 금지하던 규칙을 완화하여 허용할 수 있으나, 그렇게 하더라도 확실하게 할당된다고 보장할 수 없고, 그러한 변화는 유용해 보이지 않습니다.

this 이니셜라이저 없이 생성자 허용

this 이니셜라이저가 없는 생성자(예: 암시적 또는 명시적 base 이니셜라이저 사용)를 허용할 수 있습니다. 이러한 생성자는 인스턴스 필드, 속성 및 이벤트 이니셜라이저를 실행하지 . 이는 기본 생성자의 일부로만 간주되기 때문에 발생합니다.

이러한 기본 호출 생성자가 있는 경우 기본 생성자 매개 변수 캡처가 처리되는 방법에 대한 몇 가지 옵션이 있습니다. 가장 간단한 방법은 이 상황에서 캡처를 완전히 허용하지 않는 것입니다. 기본 생성자 매개 변수는 이러한 생성자가 있는 경우에만 초기화를 위한 매개 변수입니다.

또는 이전에 설명한 옵션과 결합하여 생성자 내의 기본 생성자 매개 변수에 대한 액세스를 허용하는 경우 매개 변수는 확실히 할당되지 않은 대로 생성자 본문을 입력할 수 있으며 캡처된 매개 변수는 생성자 본문의 끝에 의해 확실히 할당되어야 합니다. 기본적으로 암시적 출력 매개 변수입니다. 이렇게 하면 캡처된 기본 생성자 매개 변수는 다른 함수 멤버에서 사용할 때까지 항상 적절한 값(즉, 명시적으로 할당됨)을 갖습니다.

이 확장의 매력은(어느 형식으로든) 초기화되지 않은 기본 생성자 매개 변수가 관찰되는 상황으로 이어지지 않고 레코드의 "복사 생성자"에 대한 현재 예외를 완전히 일반화한다는 것입니다. 기본적으로 개체를 다른 방법으로 초기화하는 생성자는 괜찮습니다. 레코드가 기본 생성자 매개 변수(대신 필드를 생성)를 캡처하지 않으므로 캡처 관련 제한은 레코드에서 수동으로 정의된 기존 복사 생성자에 대한 호환성이 손상되는 변경이 아닙니다.

public class C(bool b, int i, string s) : B(b)
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s2) : base(true) // cannot use `string s` because it would shadow
    { 
        s = s2; // must initialize s because it is captured by S
    }
    protected C(C original) : base(original) // copy constructor
    {
        this.s = original.s; // assignment to b and i not required because not captured
    }
}

기본 생성자 본문

생성자 자체에는 매개 변수 유효성 검사 논리 또는 이니셜라이저로 표현할 수 없는 기타 사소한 초기화 코드가 포함되어 있는 경우가 많습니다.

클래스 본문에 문 블록이 직접 표시되도록 기본 생성자를 확장할 수 있습니다. 이러한 문장은 초기화 할당 사이에 등장하는 지점에서 생성된 생성자에 삽입되어, 이니셜라이저들 사이에 끼여 실행됩니다. 예컨대:

public class C(int i, string s) : B(s)
{
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }
	int[] a = new int[i];
    public int S => s;
}

생성자가 후 실행되는 "최종 이니셜라이저"를 도입하고 개체/컬렉션 이니셜라이저가 완료된 경우 이 시나리오를 충분히 다룰 수 있습니다. 그러나 인수 유효성 검사는 가능한 한 일찍 이루어지는 것이 이상적입니다.

기본 생성자 본문은 기본 생성자에 대한 접근 한정자를 설정할 수 있는 장소를 제공하여 바깥쪽 형식의 접근성과 다르게 설정할 수 있도록 할 수도 있습니다.

결합된 매개 변수 및 멤버 선언

가능하고 자주 언급되는 추가는 기본 생성자 매개 변수에 주석을 추가하여 형식에 대한 멤버를 선언할 있도록 하는 것입니다. 가장 일반적으로 매개 변수에 대한 액세스 지정자가 멤버 생성을 트리거할 수 있도록 하는 것이 제안됩니다.

public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
    void M()
    {
        ... i ... // refers to the field i
        ... s ... // closes over the parameter s
    }
}

몇 가지 문제가 있습니다.

  • 필드가 아닌 속성이 필요한 경우 어떻게 해야 할까요? 매개변수 목록에 { get; set; } 구문을 삽입하는 것은 적절하지 않은 것 같습니다.
  • 매개 변수 및 필드에 다른 명명 규칙이 사용되는 경우 어떻게 해야 할까요? 그러면 이 기능은 쓸모가 없습니다.

이것은 채택될 수도 있고, 채택되지 않을 수도 있는 잠재적인 미래 추가 사항입니다. 현재 제안은 가능성을 열어 둡니다.

질문 열기

형식 매개 변수에 대한 조회 순서

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup 섹션에서는 선언 형식의 형식 매개 변수가 해당 매개 변수가 범위에 있는 모든 컨텍스트에서 형식의 기본 생성자 매개 변수 앞에 오라고 지정합니다. 기존에 레코드와 관련하여 이미 정해진 동작이 존재하며, 기본 생성자 매개변수가 기본 이니셜라이저 및 필드 이니셜라이저에서 형식 매개변수보다 앞에 옵니다.

이러한 불일치에 대해 어떻게 해야 할까요?

  • 동작과 일치하도록 규칙을 조정합니다.
  • 동작을 조정합니다(호환성을 깨뜨릴 수 있는 변경 사항).
  • 기본 생성자 매개 변수가 형식 매개 변수의 이름(호환성이 손상되는 변경 가능)을 사용하도록 허용하지 않습니다.
  • 아무 것도 수행하지 않으며 사양과 구현 간의 불일치를 수락합니다.

결론:

동작(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors)과 일치하도록 규칙을 조정합니다.

캡처된 기본 생성자 매개 변수에 대한 필드 대상 지정 특성

캡처된 기본 생성자 매개 변수에 대한 필드 대상 지정 특성을 허용해야 하나요?

class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
    public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = x;
}

지금은 매개 변수가 캡처되었는지 여부에 관계없이 경고와 함께 특성이 무시됩니다.

레코드의 경우 속성이 합성될 때 필드 대상 특성이 허용됩니다. 그런 다음, 특성은 지원 필드에 적용됩니다.

record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = X;
}

결론:

허용되지 않음(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).

기반 클래스의 멤버에 의한 섀도잉 경고

기본 멤버가 멤버 내부에 기본 생성자 매개 변수를 숨기는 경우 경고를 보고해야 하나요(https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621참조).

결론:

대체 디자인이 승인됨 - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

클로저에서 외부 타입의 인스턴스 캡처

외부 형식의 상태에 캡처된 매개 변수가 인스턴스 초기화 블록 또는 기본 초기화 블록 내의 람다에서 참조되는 경우, 람다와 외부 형식의 상태는 매개 변수에 대해 동일한 위치를 참조해야 합니다. 예를 들어:

partial class C1
{
    public System.Func<int> F1 = Execute1(() => p1++);
}

partial class C1 (int p1)
{
    public int M1() { return p1++; }
    static System.Func<int> Execute1(System.Func<int> f)
    {
        _ = f();
        return f;
    }
}

매개 변수를 형식의 상태로 캡처하는 순진한 구현은 단순히 프라이빗 인스턴스 필드에서 매개 변수를 캡처하기 때문에 람다는 동일한 필드를 참조해야 합니다. 따라서 형식의 인스턴스에 액세스할 수 있어야 합니다. 기본 생성자가 호출되기 전에 this를 클로저에 캡처해야 합니다. 그러면 안전하기는 하지만 검증할 수 없는 IL이 발생합니다. 이것이 허용 가능한가요?

또는 다음을 수행할 수 있습니다.

  • 그런 람다를 허용하지 않습니다.
  • 또는 별도의 클래스(또 다른 클로저)의 인스턴스에서 이러한 매개변수를 캡처하고, 클로저와 관련된 타입의 인스턴스 간에 해당 인스턴스를 공유합니다. 따라서 클로저에서 this를 캡처할 필요가 없습니다.

결론:

기본 생성자가 호출되기 전에 this 닫기로 캡처하는 것이 좋습니다(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). 런타임 팀은 IL 패턴이 문제가 되지 않는다고 보았습니다.

구조체 내의 this에 할당하기

C#에서는 구조체 내의 this에 할당할 수 있습니다. 구조체가 기본 생성자 매개 변수를 캡처하는 경우 할당은 사용자에게 명확하지 않을 수 있는 값을 덮어씁니다. 이와 같은 과제에 대한 경고를 보고하시겠습니까?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

결론:

허용됨, 경고 없음(https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

초기화 및 캡처에 대한 이중 스토리지 경고

기본 생성자 매개 변수가 기본 클래스에 전달되면서 에 캡처되는 경우, 객체에 이 매개 변수가 실수로 두 번 저장될 위험이 높기 때문에 경고가 발생합니다.

매개 변수를 사용하여 멤버를 초기화하고 캡처하는 경우에도 비슷한 위험이 있는 것 같습니다. 다음은 작은 예입니다.

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

지정된 Person인스턴스의 경우, Name의 변경이 개발자가 의도하지 않은 방식으로 ToString출력에 반영되지 않을 것입니다.

이 상황에 대해 이중 스토리지 경고를 도입해야 하나요?

작동 방식은 다음과 같습니다.

컴파일러는 다음 조건이 모두 충족되면 variable_initializer 대한 경고를 생성합니다.

  • 변수 이니셜라이저는 기본 생성자 매개 변수의 암시적 또는 명시적 ID 변환을 나타냅니다.
  • 기본 생성자 매개 변수는 바깥쪽 형식의 상태로 캡처됩니다.

결론:

승인됨, https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors 참조

LDM 모임