다음을 통해 공유


공변성 반환

메모

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

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

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

요약

공변 반환 형식을 지원합니다. 특히 재정의하는 메서드보다 더 파생된 반환 형식을 선언하도록 메서드의 재정의를 허용하고, 마찬가지로 읽기 전용 속성의 재정의가 더 파생된 형식을 선언하도록 허용합니다. 더 많은 파생 형식에 표시되는 재정의 선언은 최소한 기본 형식의 재정의에 나타나는 것과 같이 특정한 반환 형식을 제공해야 합니다. 메서드 또는 속성의 호출자는 호출에서 보다 구체화된 반환 형식을 정적으로 받습니다.

동기

코드에서 자주 발견되는 패턴 중 하나는, 재정의된 메서드가 본래 메서드와 동일한 형식을 반환해야 한다는 언어 제약 조건을 해결하기 위해 다른 메서드 이름을 만들어내야 한다는 것입니다.

이는 팩터리 패턴에서 유용합니다. 예를 들어 Roslyn 코드 베이스에는

class Compilation ...
{
    public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
    public override CSharpCompilation WithOptions(Options options)...
}

상세 디자인

C#에서 공변 반환 형식에 대한 사양입니다. 메서드를 재정의하여 원래의 메서드보다 더 파생된 반환 형식을 반환할 수 있도록 하고, 마찬가지로 읽기 전용 속성을 재정의하여 원래의 속성보다 더 파생된 반환 형식을 반환할 수 있도록 하는 것이 우리의 의도입니다. 메서드 또는 속성의 호출자는 호출에서 보다 구체화된 반환 형식을 정적으로 수신하며, 더 많은 파생 형식에 나타나는 재정의는 기본 형식의 재정의에 나타나는 것과 같이 적어도 특정한 반환 형식을 제공해야 합니다.


클래스 메서드 재정의

클래스 재정의 메서드의 기존 제약 조건(§15.6.5)

  • 재정의 메서드와 재정의된 기본 메서드의 반환 형식은 동일합니다.

로 수정됨

  • 재정의 메서드에는 ID 변환으로 변환할 수 있는 반환 형식이 있어야 합니다. 또는 메서드에 값 반환이 있는 경우 재정의된 기본 메서드의 반환 형식으로의 암시적 참조 변환을 §13.1.0.5 참조할 참조 반환이 아닙니다.

그리고 다음 추가 요구 사항이 해당 목록에 추가됩니다.

  • 재정의 메서드는 ID 변환으로 변환할 수 있는 반환 형식이어야 합니다. 또는 재정의 메서드의 (직접 또는 간접) 기본 형식으로 선언된 재정의된 기본 메서드의 모든 재정의 형식에 대한 암시적 참조 변환(ref return, §13.1.0.5값 반환이 아닌 값 반환이 있는 경우).
  • 재정의 메서드의 반환 형식은 재정의된 메서드만큼 접근 가능해야 합니다 (접근성 도메인 - §7.5.3).

이 제약 조건을 사용하면 private 클래스의 재정의 메서드가 private 반환 형식을 가질 수 있습니다. 그러나 public 반환 형식을 사용하려면 public 형식의 public 재정의 메서드가 필요합니다.

클래스 속성 및 인덱서 재정의

클래스 재정의의 기존 제약 조건(§15.7.6) 속성

재정의 속성 선언은 상속된 속성과 정확히 동일한 접근성 한정자와 이름을 지정해야 하며, 재정의 형식과 상속된 속성간에 id 변환 있어야 합니다. 상속된 속성에 단일 접근자만 있는 경우(즉, 상속된 속성이 읽기 전용이거나 쓰기 전용인 경우), 재정의할 속성에는 그 접근자만 포함되어야 합니다. 상속된 속성에 두 접근자(즉, 상속된 속성이 읽기-쓰기인 경우)가 모두 포함된 경우 재정의 속성은 단일 접근자 또는 두 접근자를 모두 포함할 수 있습니다.

가 수정됨

재정의 속성 선언은 상속된 속성과 정확히 동일한 접근성 한정자와 이름을 지정해야 합니다. 및 ID 변환이 있거나(상속된 속성이 읽기 전용이고 값 반환이 있는 경우 - 참조 반환§13.1.0.5) 재정의 속성의 형식에서 상속된 속성형식으로 암시적 참조 변환이 있어야 합니다. 상속된 속성에 단일 접근자만 있는 경우(즉, 상속된 속성이 읽기 전용이거나 쓰기 전용인 경우) 재정의 속성은 해당 접근자만 포함해야 합니다. 상속된 속성에 두 접근자(즉, 상속된 속성이 읽기-쓰기인 경우)가 모두 포함된 경우 재정의 속성은 단일 접근자 또는 두 접근자를 모두 포함할 수 있습니다. 재정의하는 속성의 형식은 재정의 대상 속성(접근성 도메인 - §7.5.3) 정도까지 액세스 가능해야 합니다.


아래 초안 사양의 나머지 부분에서는 나중에 고려할 인터페이스 메서드의 공변성 반환에 대한 추가 확장을 제안합니다.

인터페이스 메서드, 속성 및 인덱서 재정의

C# 8.0에서 DIM 기능을 추가하여 인터페이스에서 허용되는 멤버 종류에 추가하면 공변 반환과 함께 override 멤버에 대한 지원이 추가됩니다. 클래스에 지정된 대로 override 멤버의 규칙을 따르며 다음과 같은 차이점이 있습니다.

클래스의 다음 텍스트는 다음과 같습니다.

재정의 선언에 의해 재정의된 메서드는재정의된 기본 메서드라고 합니다. 클래스 C에서 선언된 재정의 메서드 M 의 경우, 재정의된 기반 메서드는 C 의 직접 기반 클래스를 시작으로 하여 C의 각 기반 클래스를 순차적으로 검사하여 결정됩니다. 지정된 기반 클래스 타입에서 형식 인수를 대체한 후 M 와 동일한 시그니처를 가진 액세스 가능한 메서드가 하나 이상 발견될 때까지 계속됩니다.

인터페이스에 해당하는 관련 사양이 제공됩니다.

재정의 선언에 의해 재정의된 메서드는 재정의된 기본 메서드 로 알려져 있습니다. 인터페이스 I에서 선언된 재정의 메서드 M의 경우, 형식 인수를 대체한 후 M 서명과 동일한 서명을 가진 접근 가능한 메서드를 선언하는 인터페이스 집합을 수집하여 I의 각 직접 또는 간접 기본 인터페이스를 검사함으로써 재정의된 기본 메서드를 결정합니다. 이 인터페이스 집합에 대하여, 집합의 모든 형식에서 해당 형식으로의 ID 또는 암시적 참조 변환이 가능한 가장 많이 파생된 형식이 있고, 그 형식에 고유한 메서드 선언이 포함되어 있을 경우, 그것이재정의된 기본 메서드입니다.

마찬가지로 §15.7.6 Virtual, sealed, override 및 추상 접근자클래스에 지정된 대로 인터페이스에서 override 속성 및 인덱서를 허용합니다.

이름 조회

클래스 override 선언이 있는 경우 이름 조회는 현재 식별자의 한정자 형식에서 시작하여 클래스 계층에서 가장 파생된 override 선언에서 찾은 멤버 세부 정보를 부과하여 이름 조회 결과를 수정합니다(또는 한정자가 없는 경우 this). 예를 들어 §12.6.2.2에서 해당 매개 변수를 보유하고 있습니다.

클래스에 정의된 가상 메서드 및 인덱서의 경우 매개 변수 목록은 수신기의 정적 형식으로 시작하고 기본 클래스를 검색할 때 찾은 함수 멤버의 첫 번째 선언 또는 재정의에서 선택됩니다.

여기에 추가합니다.

인터페이스에 정의된 가상 메서드 및 인덱서의 경우 매개 변수 목록은 함수 멤버의 재정의 선언을 포함하는 형식 중 가장 파생된 형식에 있는 함수 멤버의 선언 또는 재정의에서 선택됩니다. 이러한 형식이 고유하지 않은 경우 컴파일 시간 오류입니다.

속성 또는 인덱서 액세스의 결과 형식의 경우 기존 텍스트

  • I이 인스턴스 속성을 식별하는 경우, 결과는 E과 연결된 인스턴스 식과 해당 속성의 유형인 연결된 유형을 갖는 속성 액세스입니다. T이 클래스 형식인 경우, T에서 시작해 그 기본 클래스를 통해 검색하여 찾은 속성의 첫 선언 또는 재정의에서 연결된 형식을 선택합니다.

을 사용하여 보강됨

T 인터페이스 형식인 경우 가장 파생된 T이나 그 직접 또는 간접 기본 인터페이스에서 속성의 선언 또는 재정의로부터 관련된 형식이 선택됩니다. 이러한 형식이 고유하지 않은 경우 컴파일 시간 오류입니다.

§12.8.12.3 인덱서 액세스에서 유사한 변경이 이루어져야 합니다

§12.8.10 호출 식에서 우리는 기존 텍스트를 보강합니다.

  • 그렇지 않으면 결과는 메서드 또는 대리자의 반환 형식에 해당하는 값입니다. 호출이 인스턴스 메서드이고, 수신기가 T클래스 유형인 경우, T에서 시작하여 해당 기본 클래스를 검색할 때 발견된 메서드의 첫 번째 선언이나 재정의에서 관련된 형식이 선택됩니다.

호출이 인스턴스 메서드이고 수신기가 T인터페이스 형식인 경우 연결된 형식은 T 직접 및 간접 기본 인터페이스 중에서 가장 파생된 인터페이스에 있는 메서드의 선언 또는 재정의에서 선택됩니다. 이러한 형식이 고유하지 않은 경우 컴파일 시간 오류입니다.

암시적 인터페이스 구현

사양의 이 섹션

인터페이스 매핑을 위해 A 클래스 멤버는 다음과 같은 경우 인터페이스 멤버 B 일치합니다.

  • AB 메서드이며 AB 이름, 형식 및 정식 매개 변수 목록이 동일합니다.
  • AB 속성이고, AB 이름과 형식이 동일하며, AB 동일한 접근자를 갖습니다(명시적 인터페이스 멤버 구현이 아닌 경우 추가 접근자가A 허용됨).
  • AB 이벤트이며 AB 이름과 형식은 동일합니다.
  • AB은 인덱서이고, AB은 타입 및 형식 매개변수 목록이 동일하며, AB와 동일한 접근자를 갖습니다. (명시적 인터페이스 멤버 구현이 아닌 경우A은 추가 접근자를 포함할 수 있으며, 이는 허용됩니다.)

다음과 같이 변경됩니다.

인터페이스 매핑을 위해 A 클래스 멤버는 다음과 같은 경우 인터페이스 멤버 B 일치합니다.

  • AB은 메서드이며, AB의 이름과 공식 매개변수 목록은 동일합니다. 그리고 A의 반환 유형은 암시적 참조 변환의 동일성을 통해 B의 반환 유형으로 변환할 수 있는 B의 반환 유형으로 변환할 수 있습니다.
  • AB은 속성이며, AB의 이름이 동일하고, AB와 동일한 접근자를 가집니다. 명시적 인터페이스 멤버 구현이 아닌 경우 추가 접근자는A에서 허용됩니다. 그리고 A의 형식은 ID 변환을 통해 B의 반환 형식으로 변환할 수 있으며, A가 읽기 전용 속성인 경우에는 암시적 참조 변환을 사용할 수 있습니다.
  • AB 이벤트이며 AB 이름과 형식은 동일합니다.
  • AB은 인덱서이며, AB의 공식 매개변수 목록은 동일합니다. AB와 동일한 접근자를 가집니다(명시적 인터페이스 멤버 구현이 아닌 경우에는A이 추가 접근자를 가질 수 있습니다). A의 형식은 동일성 변환을 통해 B의 반환 형식으로 변환할 수 있으며, A가 읽기 전용 인덱서일 경우 암시적 참조 변환이 가능합니다.

이는 기술적으로 호환성이 손상되는 변경입니다. 아래 프로그램이 현재는 "C1.M"을 출력하지만, 제안된 수정하에서는 "C2.M"을 출력하게 될 것입니다.

using System;

interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
    static void Main()
    {
        I1 i = new C2();
        Console.WriteLine(i.M());
    }
}

이러한 호환성이 손상되는 변경으로 인해 암시적 구현에서 공변 반환 형식을 지원하지 않는 것이 좋습니다.

인터페이스 구현에 대한 제약 조건

명시적 인터페이스 구현은 기본 인터페이스에서 재정의된 반환 형식보다 덜 파생되지 않은 반환 형식을 선언해야 한다는 규칙이 필요합니다.

API 호환성 영향

미정

열려 있는 문제

사양은 호출자가 더 구체화된 반환 형식을 가져오는 방법을 의미하지 않습니다. 아마도 호출자가 가장 많이 파생된 재정의의 매개 변수 사양을 가져오는 방법과 유사한 방식으로 수행될 수 있습니다.


다음 인터페이스가 있는 경우:

interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }

주의하세요, I3에서 메서드 I1.M()I2.M()가 "병합"되었습니다. I3구현할 때 둘 다 함께 구현해야 합니다.

일반적으로 원래 메서드를 참조하려면 명시적 구현이 필요합니다. 수업에서 질문은

class C : I1, I2, I3
{
    C IN.M();
}

여기서 무슨 뜻인가요? N는 무엇이어야 하나요?

I1.M 또는 I2.M(둘 다 아님) 구현을 허용하고 이를 둘 다의 구현으로 취급하는 것이 좋습니다.

단점

  • [ ] 모든 언어 변경은 그 자체에 대한 비용을 지불해야 합니다.
  • [ ] 깊은 상속 계층 구조의 경우에도 성능이 합리적인지 확인해야 합니다.
  • [ ] 이전 컴파일러에서 새 IL을 사용하는 경우에도 번역 전략의 아티팩트가 언어 의미 체계에 영향을 미치지 않도록 해야 합니다.

대안

소스에서 허용하도록 언어 규칙을 약간 완화할 수 있습니다.

// Possible alternative. This was not implemented.
abstract class Cloneable
{
    public abstract Cloneable Clone();
}

class Digit : Cloneable
{
    public override Cloneable Clone()
    {
        return this.Clone();
    }

    public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
    {
        return this;
    }
}

해결되지 않은 질문

  • [ ] 이 기능을 사용하기 위해 컴파일된 API는 이전 버전의 언어에서 어떻게 작동합니까?

디자인 회의