다음을 통해 공유


레코드

메모

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

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

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

챔피언 이슈: https://github.com/dotnet/csharplang/issues/39

이 제안은 C# 언어 디자인 팀에서 동의한 대로 C# 9 레코드 기능에 대한 사양을 추적합니다.

레코드의 구문은 다음과 같습니다.

record_declaration
    : attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

record_base
    : ':' class_type argument_list?
    | ':' interface_type_list
    | ':' class_type argument_list? ',' interface_type_list
    ;

record_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

레코드 형식은 클래스 선언과 유사한 참조 형식입니다. record_baseargument_list가 포함되어 있지 않은 경우, 레코드가 record_declarationparameter_list를 제공하는 것은 오류입니다. 부분 레코드의 부분 형식 선언 중 최대 하나만 parameter_list를 제공할 수 있습니다.

레코드 매개 변수는 ref, out 또는 this 한정자를 사용할 수 없습니다(inparams 허용됨).

상속

레코드는 클래스에서 상속할 수 없으며, 클래스가 object인 경우를 제외하면 클래스는 레코드에서 상속할 수 없습니다. 레코드는 다른 레코드에서 상속할 수 있습니다.

레코드 유형의 구성원

레코드 본문에 선언된 멤버 외에도 레코드 형식에는 추가 합성 멤버가 있습니다. 레코드 본문에 "일치" 서명이 있는 멤버가 선언되거나 "일치" 서명이 있는 액세스 가능한 구체적인 비 가상 멤버가 상속되지 않는 한 멤버가 합성됩니다. 일치하는 멤버는 해당 멤버를 생성하는 것을 방지하며, 컴파일러가 다른 합성 멤버들을 생성하는 것에는 영향을 미치지 않습니다. 두 멤버는 서명이 동일하거나 상속 시나리오에서 '숨기기'로 간주될 수 있는 경우 일치하는 것으로 간주됩니다. 레코드의 멤버 이름을 "Clone"으로 지정하는 것은 오류입니다. 레코드의 인스턴스 필드에 최상위 포인터 형식이 있는 것은 오류입니다. 포인터 배열과 같은 중첩 포인터 형식이 허용됩니다.

합성 멤버는 다음과 같습니다.

평등 회원

레코드가 object파생된 경우 레코드 형식에는 다음과 같이 선언된 속성에 해당하는 합성된 읽기 전용 속성이 포함됩니다.

Type EqualityContract { get; }

레코드 형식이 private경우 속성이 sealed. 그렇지 않으면 속성은 virtual 그리고 protected입니다. 속성을 명확하게 선언할 수 있습니다. 명시적 선언이 예상된 서명 또는 접근성과 일치하지 않거나 명시적 선언이 파생 형식에서 재정의를 허용하지 않고 레코드 형식이 sealed않은 경우 오류가 발생합니다.

레코드 형식이 Base기본 레코드 형식에서 파생되는 경우 레코드 형식에는 다음과 같이 선언된 속성에 해당하는 합성된 읽기 전용 속성이 포함됩니다.

protected override Type EqualityContract { get; }

속성을 명확하게 선언할 수 있습니다. 명시적 선언이 예상된 서명 또는 접근성과 일치하지 않거나 명시적 선언이 파생 형식에서 재정의를 허용하지 않고 레코드 형식이 sealed않은 경우 오류가 발생합니다. 합성되거나 명시적으로 선언된 속성이 레코드 유형 Base의 서명으로 속성을 재정의하지 않으면 그것은 오류입니다(예: 속성이 Base에 누락되었거나, 봉인되었거나, 가상이 아닌 경우). 합성된 속성은 typeof(R)이 레코드 형식일 때 R를 반환합니다.

레코드 형식은 System.IEquatable<R>를 구현하고, Equals(R? other)가 레코드 형식인 R의 합성된 강력한 형식의 오버로드를 포함합니다. 메서드는 레코드 형식이 public가 아닌 한 virtual이며, 그렇지 않으면 sealed입니다. 메서드를 명시적으로 선언할 수 있습니다. 명시적 선언이 예상된 서명이나 접근성과 일치하지 않거나, 명시적 선언이 파생 형식에서 재정의될 수 없으며 레코드 형식이 sealed이 아닌 경우, 이는 오류입니다.

Equals(R? other) 사용자 정의(합성되지 않음)이지만 GetHashCode 아닌 경우 경고가 생성됩니다.

public virtual bool Equals(R? other);

다음 조건이 모두 Equals(R?)인 경우에만 합성된 truetrue을 반환합니다.

  • othernull가 아닙니다, 그리고
  • 상속되지 않은 레코드 형식의 각 인스턴스 필드 fieldN에 대해, 필드 유형이 System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)인 경우의 값 TN
  • 기본 레코드 형식이 있는 경우 base.Equals(other)의 비가상 호출인 public virtual bool Equals(Base? other)의 값입니다. 그렇지 않으면 EqualityContract == other.EqualityContract의 값입니다.

레코드 형식에는 다음과 같이 선언된 연산자와 동일한 합성된 ==!= 연산자가 포함됩니다.

public static bool operator==(R? left, R? right)
    => (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
    => !(left == right);

Equals 연산자가 호출하는 == 메서드는 위에서 지정한 Equals(R? other) 메서드입니다. != 연산자는 == 연산자에 위임합니다. 연산자가 명시적으로 선언된 경우 오류입니다.

레코드 형식이 Base기본 레코드 형식에서 파생되는 경우 레코드 형식에는 다음과 같이 선언된 메서드에 해당하는 합성 재정의가 포함됩니다.

public sealed override bool Equals(Base? other);

재정의가 명시적으로 선언된 경우 오류가 발생합니다. 오류입니다. 메서드가 레코드 형식 Base 에서 동일한 시그니처를 가진 메서드를 재정의하지 않는 경우(예: 메서드가 Base에 누락되었거나, 봉인되었거나, 가상 메서드가 아닌 경우). 합성된 오버라이드는 Equals((object?)other)을 반환합니다.

레코드 형식에는 다음과 같이 선언된 메서드에 해당하는 합성된 재정의가 포함됩니다.

public override bool Equals(object? obj);

재정의가 명시적으로 선언된 경우 오류가 발생합니다. 메서드가 object.Equals(object? obj)을 재정의하지 않는 경우(예: 중간 기본 유형에서의 그림자 효과 등)는 오류입니다. 합성된 재정의는 Equals(other as R)이 레코드 형식일 때 R을 반환합니다.

레코드 형식에는 다음과 같이 선언된 메서드에 해당하는 합성된 재정의가 포함됩니다.

public override int GetHashCode();

메서드를 명시적으로 선언할 수 있습니다. 명시적 선언이 파생된 형식에서 재정의를 허용하지 않고 레코드 형식이 sealed이 아니면 오류가 발생합니다. 합성되거나 명시적으로 선언된 메서드가 object.GetHashCode()를 재정의하지 않는 경우(예: 중간 기본 형식에서의 가리기 등) 이는 오류입니다.

Equals(R?)GetHashCode() 중 하나가 명시적으로 선언되었지만 다른 메서드가 명시적이지 않은 경우 경고가 보고됩니다.

GetHashCode()의 합성된 재정의는 다음 값을 결합하여 int 결과를 반환합니다.

  • 상속되지 않은 레코드 형식의 각 인스턴스 필드 fieldN에 대해, 필드 유형이 System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)인 경우의 값 TN
  • 기본 레코드 형식이 있는 경우 값은 base.GetHashCode(); 그렇지 않으면 System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)값입니다.

예를 들어 다음 레코드 유형을 고려합니다.

record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);

이러한 레코드 형식의 경우 합성된 동등성 멤버는 다음과 같습니다.

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    protected virtual Type EqualityContract => typeof(R1);
    public override bool Equals(object? obj) => Equals(obj as R1);
    public virtual bool Equals(R1? other)
    {
        return !(other is null) &&
            EqualityContract == other.EqualityContract &&
            EqualityComparer<T1>.Default.Equals(P1, other.P1);
    }
    public static bool operator==(R1? left, R1? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R1? left, R1? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
            EqualityComparer<T1>.Default.GetHashCode(P1));
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    protected override Type EqualityContract => typeof(R2);
    public override bool Equals(object? obj) => Equals(obj as R2);
    public sealed override bool Equals(R1? other) => Equals((object?)other);
    public virtual bool Equals(R2? other)
    {
        return base.Equals((R1?)other) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R2? left, R2? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R2? left, R2? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

class R3 : R2, IEquatable<R3>
{
    public T3 P3 { get; init; }
    protected override Type EqualityContract => typeof(R3);
    public override bool Equals(object? obj) => Equals(obj as R3);
    public sealed override bool Equals(R2? other) => Equals((object?)other);
    public virtual bool Equals(R3? other)
    {
        return base.Equals((R2?)other) &&
            EqualityComparer<T3>.Default.Equals(P3, other.P3);
    }
    public static bool operator==(R3? left, R3? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R3? left, R3? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T3>.Default.GetHashCode(P3));
    }
}

멤버 복사 및 복제

레코드 형식에는 두 개의 복사 멤버가 포함됩니다.

  • 레코드 형식의 단일 인수를 사용하는 생성자입니다. 이를 "복사 생성자"라고 합니다.
  • 매개변수가 없는 공용 인스턴스 "clone" 메서드가 컴파일러에 의해 예약된 이름으로 합성되었습니다.

복사 생성자의 목적은 매개 변수에서 생성되는 새 인스턴스로 상태를 복사하는 것입니다. 이 생성자는 레코드 선언에 있는 인스턴스 필드/속성 이니셜라이저를 실행하지 않습니다. 생성자가 명시적으로 선언되지 않은 경우 생성자는 컴파일러에 의해 합성됩니다. 레코드가 봉인되면 생성자는 프라이빗이 되고, 그렇지 않으면 보호됩니다. 레코드가 봉인되지 않는 한 명시적으로 선언된 복사 생성자는 public이거나 보호되어야 합니다. 생성자가 수행해야 하는 첫 번째 작업은 기본의 복사 생성자를 호출하거나 레코드가 개체에서 상속되는 경우 매개 변수가 없는 개체 생성자를 호출하는 것입니다. 사용자 정의 복사 생성자가 이 요구 사항을 충족하지 않는 암시적 또는 명시적 생성자 이니셜라이저를 사용하는 경우 오류가 보고됩니다. 기본 복사 생성자를 호출한 후 합성된 복사 생성자는 레코드 형식 내에서 암시적으로 또는 명시적으로 선언된 모든 인스턴스 필드에 대한 값을 복사합니다. 명시적 또는 암시적 복사 생성자의 유일한 존재는 기본 인스턴스 생성자의 자동 추가를 방지하지 않습니다.

기반 레코드에 가상 "clone" 메서드가 있는 경우, 생성된 "clone" 메서드가 이를 재정의하며, 메서드의 반환 형식은 현재 포함된 형식입니다. 기본 레코드 복제 메서드가 봉인되면 오류가 발생합니다. 가상 "clone" 메서드가 기본 레코드에 없는 경우 클론 메서드의 반환 형식은 포함하는 형식이며 레코드가 봉인되거나 추상화되지 않는 한 메서드는 가상입니다. 포함하는 레코드가 추상인 경우 합성된 클론 메서드도 추상적입니다. "clone" 메서드가 추상이 아니면 복사 생성자에 대한 호출 결과를 반환합니다.

출력 멤버: PrintMembers 및 ToString 메서드

레코드가 object에서 파생된 경우, 레코드에는 다음과 같이 선언된 메서드와 동일한 합성된 메서드가 포함됩니다.

bool PrintMembers(System.Text.StringBuilder builder);

레코드 형식이 private경우 메서드가 sealed. 그렇지 않으면 메서드는 virtualprotected입니다.

메서드:

  1. 메서드가 존재하고 레코드에 인쇄 가능한 멤버가 있는 경우, 메서드 System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack()을 호출합니다.
  2. 각 레코드의 출력 가능한 멤버(비정적 공용 필드 및 읽을 수 있는 속성 멤버)에 대해, 해당 멤버의 이름 뒤에 " = "를 추가하고 그 다음에 멤버의 값을 ", "로 구분하여 추가합니다.
  3. 는 레코드에 인쇄 가능한 멤버가 있는 경우 true를 반환합니다.

값 형식이 있는 멤버의 경우 대상 플랫폼에서 사용할 수 있는 가장 효율적인 방법을 사용하여 해당 값을 문자열 표현으로 변환합니다. 현재는 ToString에 전달하기 전에 StringBuilder.Append을 호출하는 것을 의미합니다.

기록 형식이 기본 레코드 Base에서 파생된 경우, 레코드에는 다음과 같이 선언된 메서드에 해당하는 합성 재정의가 포함됩니다.

protected override bool PrintMembers(StringBuilder builder);

레코드에 인쇄 가능한 멤버가 없으면 메서드는 하나의 인수(PrintMembers 매개 변수)를 사용하여 기본 builder 메서드를 호출하고 결과를 반환합니다.

그렇지 않으면 메서드:

  1. 는 하나의 인수(PrintMembers 매개 변수)를 사용하여 기본 builder 메서드를 호출합니다.
  2. PrintMembers 메서드가 true를 반환하면 빌더에 ", "를 추가합니다.
  3. 각 레코드의 출력 가능한 멤버에 대해, 해당 멤버의 이름 뒤에 " = "를 추가하고, 그 뒤에 멤버의 값을 추가합니다: this.member (값 형식인 경우 this.member.ToString()). 각 항목은 ", "로 구분됩니다.
  4. true를 반환합니다.

PrintMembers 메서드를 명시적으로 선언할 수 있습니다. 명시적 선언이 예상된 서명 또는 접근성과 일치하지 않거나 명시적 선언이 파생 형식에서 재정의를 허용하지 않고 레코드 형식이 sealed않은 경우 오류가 발생합니다.

레코드에는 다음과 같이 선언된 메서드에 해당하는 합성된 메서드가 포함됩니다.

public override string ToString();

메서드를 명시적으로 선언할 수 있습니다. 명시적 선언이 예상된 서명 또는 접근성과 일치하지 않거나 명시적 선언이 파생 형식에서 재정의를 허용하지 않고 레코드 형식이 sealed않은 경우 오류가 발생합니다. 합성되거나 명시적으로 선언된 메서드가 object.ToString()를 재정의하지 않는 경우(예: 중간 기본 형식에서의 가리기 등) 이는 오류입니다.

합성된 메서드:

  1. StringBuilder 인스턴스를 생성합니다.
  2. 작성기에서 레코드 이름을 추가한 다음 " { ",
  3. 레코드의 PrintMembers 메서드를 호출하여 작성기를 제공하고, true를 반환하면 " "을 추가합니다.
  4. "}"를 추가합니다.
  5. 작성기의 콘텐츠를 builder.ToString()으로 반환합니다.

예를 들어 다음 레코드 유형을 고려합니다.

record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);

이러한 레코드 형식의 경우 합성된 인쇄 구성 요소는 다음과 같습니다.

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    public T3 P3 { get; init; }
    
    protected override bool PrintMembers(StringBuilder builder)
    {
        if (base.PrintMembers(builder))
            builder.Append(", ");
            
        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
        
        builder.Append(", ");
        
        builder.Append(nameof(P3));
        builder.Append(" = ");
        builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R2));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

위치 레코드 멤버

위의 멤버 외에도 매개 변수 목록("위치 레코드")이 있는 레코드는 위의 멤버와 동일한 조건으로 추가 멤버를 합성합니다.

기본 생성자

레코드 형식에는 형식 선언의 값 매개 변수에 해당하는 서명이 있는 공용 생성자가 있습니다. 이를 형식의 주 생성자라고 하며, 존재할 경우 암시적으로 선언된 기본 클래스 생성자가 억제됩니다. 기본 생성자와 동일한 서명이 있는 생성자가 클래스에 이미 있는 것은 오류입니다.

런타임에 기본 생성자

  1. 클래스 본문에 나타나는 인스턴스 초기화기를 실행합니다.

  2. 있는 경우 record_base 절에 제공된 인수를 사용하여 기본 클래스 생성자를 호출합니다.

레코드에 기본 생성자가 있는 경우 "복사 생성자"를 제외한 모든 사용자 정의 생성자에는 명시적 this 생성자 이니셜라이저가 있어야 합니다.

레코드의 멤버뿐만 아니라 기본 생성자의 매개 변수는 argument_list 절의 record_base 범위 내에 있으며 인스턴스 필드 또는 속성의 이니셜라이저 내에 있습니다. 인스턴스 멤버는 이러한 위치에서 사용하려고 하면 오류가 발생하지만(현재 일반 생성자 이니셜라이저의 범위에 있는 인스턴스 멤버처럼) 주 생성자의 매개변수는 범위 내에 있으면서 사용 가능하고, 이로 인해 멤버를 가립니다. 정적 멤버는 현재 일반 생성자에서 기본 호출 및 이니셜라이저가 작동하는 방식과 유사하게 사용할 수 있습니다.

기본 생성자의 매개 변수를 읽지 않으면 경고가 생성됩니다.

argument_list에 선언된 식 변수는 argument_list범위 내에서 유효합니다. 일반 생성자 이니셜라이저의 인수 목록 내에서와 동일한 섀도링 규칙이 적용됩니다.

속성

레코드 형식 선언의 각 레코드 매개 변수에는 값 매개 변수 선언에서 이름과 형식을 가져온 해당 public 속성 멤버가 있습니다.

기록용으로:

  • 공용 getinit 자동 속성이 생성됩니다(별도의 init 접근자 사양 참고하십시오). 형식이 일치하는 상속된 abstract 속성이 재정의됩니다. 상속된 속성에 재정의 가능한 publicgetinit 접근자가 없는 경우 오류가 발생합니다. 상속된 속성이 숨겨져 있으면 오류가 발생합니다.
    자동 속성은 해당 기본 생성자 매개 변수의 값으로 초기화됩니다. 특성은 해당 레코드 매개 변수에 구문적으로 적용된 특성에 대해 property: 또는 field: 대상을 사용하여 합성된 자동 속성 및 해당 지원 필드에 적용할 수 있습니다.

분해

매개 변수가 하나 이상 있는 위치 레코드는 기본 생성자 선언의 각 매개 변수에 대한 out 매개 변수 선언을 사용하여 Deconstruct라는 public void 반환 인스턴스 메서드를 합성합니다. Deconstruct 메서드의 각 매개 변수에는 기본 생성자 선언의 해당 매개 변수와 동일한 형식이 있습니다. 메서드의 본문은 각 매개 변수에 동일한 이름을 가진 인스턴스 속성 값을 Deconstruct 메서드로 할당합니다. 메서드를 명시적으로 선언할 수 있습니다. 명시적 선언이 예상된 서명 또는 접근성과 일치하지 않거나 정적이면 오류가 발생합니다.

다음 예제에서는 컴파일러가 합성된 R 메서드와 해당 사용법을 사용하여 위치 레코드 Deconstruct 보여 줍니다.

public record R(int P1, string P2 = "xyz")
{
    public void Deconstruct(out int P1, out string P2)
    {
        P1 = this.P1;
        P2 = this.P2;
    }
}

class Program
{
    static void Main()
    {
        R r = new R(12);
        (int p1, string p2) = r;
        Console.WriteLine($"p1: {p1}, p2: {p2}");
    }
}

with

with 식은 다음 구문을 사용하는 새 식입니다.

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

with 식은 문장으로 사용할 수 없습니다.

with 표현식은 member_initializer_list할당에서 수정된 수신 표현식의 복사본을 생성하도록 설계된 "비파괴적 변이"를 허용합니다.

유효한 with 식에는 void 형식이 아닌 수신기가 있습니다. 수신기 형식은 레코드여야 합니다.

with 식의 오른쪽에는 일련의 할당이 포함된 member_initializer_list이 있으며, 이 할당은 수신기 형식의 액세스 가능한 인스턴스 필드 또는 속성인 식별자에 해당해야 합니다.

첫째, 수신기의 "clone" 메서드(위에서 지정됨)가 호출되고 그 결과가 수신기의 형식으로 변환됩니다. 그런 다음 각 member_initializer은 변환 결과의 필드 또는 속성 액세스에 대한 할당과 동일한 방식으로 처리됩니다. 할당은 어휘 순서로 처리됩니다.