다음을 통해 공유


자동 기본 구조체

메모

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

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

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

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

요약

이 기능은 구조체 생성자 내에서 사용자가 반환하거나 사용하기 전에 명시적으로 할당하지 않은 필드를 식별하고, 명시적 할당 오류를 주는 대신에 해당 필드를 암시적으로 default으로 초기화합니다.

동기

이 제안은 dotnet/csharplang#5552 및 dotnet/csharplang#5635에서 발견된 유용성 문제에 대한 가능한 완화와 #5563(모든 필드는 반드시 할당되어야 하지만 field 생성자 내에서 액세스할 수 없음)을 해결하기 위한 가능한 완화 방법으로 제기됩니다.


C# 1.0 이후로, 구조체 생성자는 thisout 매개 변수인 것처럼 확실히 할당해야 했습니다.

public struct S
{
    public int x, y;
    public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
    {
    }
}

컴파일러에서 속성 할당을 지원 필드 할당과 동일하게 처리할 수 없으므로 setter가 세미 자동 속성에서 수동으로 정의되는 경우 문제가 발생합니다.

public struct S
{
    public int X { get => field; set => field = value; }
    public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
    {
    }
}

setter가 ref this 대신 out field을 매개 변수로 사용하는 방식과 같은 세분화된 제한을 도입하는 것은 일부 사용 사례에는 너무 특수해서 불완전할 것이라고 가정합니다.

우리가 어려움을 겪고 있는 근본적인 문제 중 하나는 구조체 속성에 수동으로 setter를 구현할 때 사용자가 논리 할당이나 로직을 반복적으로 수행해야 하는 경우가 많다는 점입니다.

struct S
{
    private int _x;
    public int X
    {
        get => _x;
        set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
    }

    // Solution 1: assign some value in the constructor before "really" assigning through the property setter.
    public S(int x)
    {
        _x = default;
        X = x;
    }

    // Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
    public S(int x)
    {
        _x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
    }
}

이전 토론

소규모 그룹은 이 문제를 살펴보고 몇 가지 가능한 해결 방법을 고려했습니다.

  1. 사용자가 세미 자동 속성에 대해 setter를 수동으로 구현할 경우 this = default를 할당하도록 요구합니다. 필드 이니셜라이저에 설정된 값을 날려버리기 때문에 잘못된 솔루션이라는 데 동의합니다.
  2. 자동/반자동 속성의 모든 지원 필드를 암시적으로 초기화합니다.
    • "이렇게 하면 '반자동 속성 설정자' 문제를 해결하고, 명시적으로 선언된 필드는 다른 규칙에 따라 적용됩니다: '내 필드를 암시적으로 초기화하지 말고, 내 자동 속성은 암시적으로 초기화하십시오.'"
  3. 세미 자동 속성의 지원 필드를 할당할 수 있는 방법을 제공하며, 사용자가 그 필드를 반드시 할당하도록 요구합니다.
    • 이는 (2)에 비해 번거로울 수 있습니다. 자동 속성은 "자동"이어야 하며 필드의 "자동" 초기화를 포함할 수 있습니다. 기본 필드가 속성 할당에 의해 설정되는 시기와 속성의 setter가 호출되는 시기에 대한 혼란이 생길 수 있습니다.

우리는 예를 들어, 모든 항목을 명시적으로 할당하지 않고도 구조체에 몇 가지 필드 이니셜라이저를 포함하고 싶어하는 사용자로부터 피드백을 받았습니다. 이 문제뿐만 아니라 "수동으로 구현된 setter를 사용한 반자동 속성" 문제를 동시에 해결할 수 있습니다.

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

확정 할당 조정

this할당되지 않은 필드에 오류를 주는 대신, 우리는 "확정 할당 분석"을 수행하여 암시적으로 초기화해야 하는 필드를 확인합니다. 이러한 초기화는 생성자시작 부분에 삽입됩니다.

struct S
{
    int x, y;

    // Example 1
    public S()
    {
        // ok. Compiler inserts an assignment of `this = default`.
    }

    // Example 2
    public S()
    {
        // ok. Compiler inserts an assignment of `y = default`.
        x = 1;
    }

    // Example 3
    public S()
    {
        // valid since C# 1.0. Compiler inserts no implicit assignments.
        x = 1;
        y = 2;
    }

    // Example 4
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `this = default`.
        if (b)
            x = 1;
        else
            y = 2;
    }

    // Example 5
    void M() { }
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `y = default`.
        x = 1;
        if (b)
            M();

        y = 2;
    }
}

예제(4) 및 (5)에서 결과 codegen에는 필드의 "이중 할당"이 있는 경우가 있습니다. 일반적으로는 괜찮지만, 이러한 이중 할당에 관심 있는 사용자의 경우, 확정 할당 오류 진단을 기본적으로 사용 중지된 경고 진단으로 발생시킬 수 있습니다.

struct S
{
    int x;
    public S() // warning: 'S.x' is implicitly initialized to 'default'.
    {
    }
}

이 진단의 심각도를 "오류"로 설정한 사용자는 C# 11 이전 동작을 옵트인합니다. 이러한 사용자는 수동으로 구현된 setter로 인해 반자동 속성에서 기본적으로 "차단"됩니다.

struct S
{
    public int X
    {
        get => field;
        set => field = field < value ? value : field;
    }

    public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
    {
        X = 1;
    }
}

처음 보기에는 이것이 기능의 '구멍'처럼 느껴지지만, 실제로는 하는 것이 올바른 것입니다. 진단을 사용하도록 설정하면 사용자는 컴파일러가 생성자에서 해당 필드를 암시적으로 초기화하지 않도록 할 것을 알려 줍니다. 여기서 암시적 초기화를 방지할 수 있는 방법은 없으므로 이를 위한 솔루션은 수동으로 구현된 setter와는 다른 방식으로 필드를 초기화하는 것입니다(예: 수동으로 필드를 선언하고 할당하거나 필드 이니셜라이저를 포함).

현재 JIT는 refs를 통해 데드 저장소를 제거하지 않습니다. 즉, 이러한 암시적 초기화에는 실제 비용이 있습니다. 그러나 그것은 고칠 수 있습니다. https://github.com/dotnet/runtime/issues/13727

전체 인스턴스가 아닌 개별 필드를 초기화하는 것은 실제로 최적화에 불과하다는 다는 점을 주목할 가치가 있습니다. 컴파일러는 모든 반환 지점에서 확실히 할당되지 않은 필드나 this의 비필드 멤버가 액세스되기 전에 암시적으로 초기화되는 불변 조건을 만족하는 한, 원하는 어떤 휴리스틱이든 자유롭게 구현할 수 있어야 합니다.

예를 들어 구조체에 100개의 필드가 있고 그 중 하나만 명시적으로 초기화되는 경우 99개의 다른 필드에 대한 initobj 암시적으로 내보내는 것보다 전체 항목에서 initobj 수행하는 것이 더 합리적일 수 있습니다. 그러나 99개의 다른 필드에 대해 initobj을 암시적으로 출력하는 구현도 여전히 유효합니다.

언어 사양 변경

표준의 다음 섹션을 조정합니다.

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access

생성자 선언에 생성자 이니셜라이저가 없는 경우 this 변수는 구조체 형식의 out 매개 변수와 정확히 동일하게 동작합니다. 특히 이는 변수가 인스턴스 생성자의 모든 실행 경로에 확실히 할당되어야 하다는 것을 의미합니다.

이 언어가 읽히도록 조정합니다.

생성자 선언에 생성자 이니셜라이저가 없는 경우 this 변수는 명확한 할당 요구 사항(out)이 충족되지 않는 경우 오류가 아니라는 점을 제외하고 구조체 형식의 매개 변수와 유사하게 동작합니다. 대신 다음과 같은 동작을 소개합니다.

  1. this 변수 자체가 요구 사항을 충족하지 않는 경우 요구 사항을 위반하는 모든 지점에서 this 내의 할당되지 않은 모든 인스턴스 변수는 생성자의 다른 코드가 실행되기 전에 초기화 단계에서 기본값(§9.3)으로 암시적으로 초기화됩니다.
  2. 내의 인스턴스 변수 this 요구 사항을 충족하지 않거나 v 내 중첩 수준의 인스턴스 변수가 요구 사항을 충족하지 않는 경우 v 생성자의 다른 코드가 실행되기 전에 초기화 단계의 기본값으로 암시적으로 초기화됩니다.

디자인 회의

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs