공변성 반환
메모
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 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
일치합니다.
A
및B
메서드이며A
및B
이름, 형식 및 정식 매개 변수 목록이 동일합니다.A
및B
속성이고,A
및B
이름과 형식이 동일하며,A
B
동일한 접근자를 갖습니다(명시적 인터페이스 멤버 구현이 아닌 경우 추가 접근자가A
허용됨).A
및B
이벤트이며A
및B
이름과 형식은 동일합니다.A
및B
은 인덱서이고,A
및B
은 타입 및 형식 매개변수 목록이 동일하며,A
는B
와 동일한 접근자를 갖습니다. (명시적 인터페이스 멤버 구현이 아닌 경우A
은 추가 접근자를 포함할 수 있으며, 이는 허용됩니다.)
다음과 같이 변경됩니다.
인터페이스 매핑을 위해
A
클래스 멤버는 다음과 같은 경우 인터페이스 멤버B
일치합니다.
A
및B
은 메서드이며,A
및B
의 이름과 공식 매개변수 목록은 동일합니다. 그리고A
의 반환 유형은 암시적 참조 변환의 동일성을 통해B
의 반환 유형으로 변환할 수 있는B
의 반환 유형으로 변환할 수 있습니다.A
및B
은 속성이며,A
및B
의 이름이 동일하고,A
는B
와 동일한 접근자를 가집니다. 명시적 인터페이스 멤버 구현이 아닌 경우 추가 접근자는A
에서 허용됩니다. 그리고A
의 형식은 ID 변환을 통해B
의 반환 형식으로 변환할 수 있으며,A
가 읽기 전용 속성인 경우에는 암시적 참조 변환을 사용할 수 있습니다.A
및B
이벤트이며A
및B
이름과 형식은 동일합니다.A
및B
은 인덱서이며,A
와B
의 공식 매개변수 목록은 동일합니다.A
는B
와 동일한 접근자를 가집니다(명시적 인터페이스 멤버 구현이 아닌 경우에는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는 이전 버전의 언어에서 어떻게 작동합니까?
디자인 회의
- https://github.com/dotnet/roslyn/issues/357에서 몇 가지 논의.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- C# 9.0에서만 클래스 메서드 재정의를 지원하기로 한 결정에 대한 오프라인 토론입니다.
C# feature specifications