공분산과 공분산이 값 유형을 지원하지 않는 이유


149

IEnumerable<T>공동 변종 하지만 값 유형, 단지에만 참조 형식을 지원하지 않습니다. 아래 간단한 코드가 성공적으로 컴파일되었습니다.

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

그러나에서 string로 변경 int하면 컴파일 오류가 발생합니다.

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

그 이유는 MSDN에 설명되어 있습니다 .

차이는 참조 유형에만 적용됩니다. 변형 유형 매개 변수에 값 유형을 지정하면 해당 유형 매개 변수는 생성 된 생성 유형에 대해 변하지 않습니다.

나는 그 이유가 가치 유형과 참조 유형 사이의 권투 라고 언급 한 일부 질문을 검색하고 발견했습니다 . 그러나 왜 권투가 그 이유인지에 대해 많은 생각이 들지 않습니다.

공분산과 공분산이 왜 가치 유형을 지원하지 않는지, 그리고 권투가 어떻게 영향을 미치는지 간단하고 자세한 설명을 해 줄 수 있습니까?


3
비슷한 질문에 대한 Eric의 답변을 참조하십시오 : stackoverflow.com/questions/4096299/…
thorn̈

답변:


126

기본적으로 차이는 CLR 이 값을 표현 적으로 변경할 필요가 없음을 보장 할 수있는 경우에 적용됩니다 . 참조는 모두 동일하게 보이므로 표시를 변경하지 않고 IEnumerable<string>로 사용할 수 있습니다 IEnumerable<object>. 기본 코드 자체는 인프라가 확실히 유효하다는 것을 보장하는 한 값으로 수행중인 작업을 전혀 알 필요가 없습니다.

값 유형의 경우, 작동하지 않는 - 치료 IEnumerable<int>가 AS를 IEnumerable<object>, 순서를 사용하여 코드는 권투 변환을 수행할지 여부를 알고 있어야합니다.

일반적으로이 주제에 대한 자세한 내용 Eric Lippert의 블로그 표현 및 정체성에 대한 내용을 참조하십시오.

편집 : Eric의 블로그 게시물을 다시 읽은 후에 는 둘이 연결되어 있지만 적어도 표현만큼 정체성 에 관한 것 입니다. 특히:

인터페이스 및 델리게이트 유형의 공변량 및 반 변형 변환에서 모든 가변 유형 인수가 참조 유형이어야합니다. 변형 참조 변환이 항상 ID 보존인지 확인하려면 형식 인수와 관련된 모든 변환도 ID 보존이어야합니다. 형식 인수에 대한 사소하지 않은 모든 변환이 ID 보존임을 확인하는 가장 쉬운 방법은 참조 변환으로 제한하는 것입니다.


5
@ CuongLe : 글쎄 그것은 어떤 의미에서 구현 세부 사항이지만 제한의 근본 원인이라고 생각합니다.
Jon Skeet

2
@ AndréCaron : Eric의 블로그 게시물은 여기서 중요합니다. 이는 표현뿐만 아니라 ID 보존이기도합니다. 그러나 표현 보존은 생성 된 코드가이를 전혀 신경 쓸 필요가 없음을 의미합니다.
Jon Skeet

1
정확하게 int는의 하위 유형이 아니기 때문에 ID를 보존 할 수 없습니다 object. 표현상의 변화가 필요하다는 사실은 이것의 결과 일뿐입니다.
André Caron

3
int는 어떻게 객체의 하위 유형이 아닌가? Int32는 System.Object에서 상속되는 System.ValueType에서 상속합니다.
David Klempfner

1
@DavidKlempfner @ AndréCaron 의견이 잘못 표현 된 것 같습니다. 와 같은 모든 값 유형 Int32에는 "boxed"와 "unboxed"라는 두 가지 표현 형식이 있습니다. 일반적으로 소스 코드 수준에서는 보이지 않지만 컴파일러는 한 형식에서 다른 형식으로 변환 할 코드를 삽입해야합니다. 사실상, 기본 시스템에서는 "박스형"형식 만 하위 유형으로 간주 object하지만 컴파일러는 값 유형이 호환 가능한 인터페이스 또는 유형에 할당 될 때마다이를 자동으로 처리합니다 object.
스티브

10

기본 표현에 대해 생각하면 이해하기가 더 쉽습니다 (실제로 구현 세부 사항 임에도 불구하고). 다음은 문자열 모음입니다.

IEnumerable<string> strings = new[] { "A", "B", "C" };

strings다음을 나타내는 것으로 생각할 수 있습니다 .

[0] : 문자열 참조-> "A"
[1] : 문자열 참조-> "B"
[2] : 문자열 참조-> "C"

세 가지 요소의 모음으로, 각각은 문자열에 대한 참조입니다. 이것을 객체 컬렉션으로 캐스트 할 수 있습니다.

IEnumerable<object> objects = (IEnumerable<object>) strings;

기본적으로 참조는 객체 참조라는 점을 제외하면 동일한 표현입니다.

[0] : 객체 참조-> "A"
[1] : 객체 참조-> "B"
[2] : 객체 참조-> "C"

표현은 동일합니다. 참고 문헌은 다르게 취급됩니다. 더 이상 string.Length속성에 액세스 할 수 없지만 여전히 전화를 걸 수 있습니다 object.GetHashCode(). 이것을 int 모음과 비교하십시오.

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0] : int = 1
[1] : int = 2
[2] : int = 3

이것을 IEnumerable<object>int로 변환하여 데이터 를 변환해야합니다 .

[0] : 객체 참조-> 1
[1] : 객체 참조-> 2
[2] : 객체 참조-> 3

이 변환에는 캐스트 이상이 필요합니다.


2
권투는 단지 "구현 세부 사항"이 아닙니다. 박스형 값 유형은 클래스 객체와 동일한 방식으로 저장되며 외부 객체가 클래스 객체처럼 알 수있는 한 동작합니다. 유일한 차이점은 박스형 값 유형의 정의 내에서 this필드를 보유하는 객체를 참조하는 대신 필드가 저장하는 힙 객체의 필드를 오버레이하는 구조체를 참조한다는 것입니다. 박스형 값 유형 인스턴스가 둘러싸는 힙 객체에 대한 참조를 얻는 명확한 방법은 없습니다.
supercat

7

나는 모든 것이 LSP(Liskov Substitution Principle)의 정의에서 시작한다고 생각합니다 .

q (x)가 유형 T의 객체 x에 대해 가능한 속성 인 경우, S (T)의 하위 유형 인 S 유형의 객체 y에 대해 q (y)는 true 여야합니다.

그러나 예를 들어 값 유형 intobject에서를 대신 할 수 없습니다 C#. 증명은 매우 간단합니다.

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

객체에 동일한 "참조"를 false할당하더라도 반환 됩니다 .


1
나는 당신이 올바른 원칙을 사용하고 있다고 생각하지만 증거 int가 없습니다 : 의 하위 유형이 object아니기 때문에 원칙이 적용되지 않습니다. 당신의 "증거"는 중간 표현에 의존 Integer하는데,이 표현 object은 언어의 암시 적 변환 의 하위 유형 object obj1=myInt;이며 실제로는 object obj1=new Integer(myInt);로 확장됩니다 .
André Caron

언어는 유형간에 올바른 캐스트를 처리하지만 int 동작은 객체의 하위 유형에서 예상되는 것과 일치하지 않습니다.
Tigran

내 요점은 정확하게 int의 하위 유형이 아닙니다 object. 또한, LSP가 있기 때문에 적용되지 않습니다 myInt, obj1그리고 obj2세 가지 다른 개체를 참조 : 하나 int와 두 (숨겨진) Integer이야.
André Caron

22
@ André : C #은 Java가 아닙니다. C #의 int키워드는 BCL의 별칭 System.Int32이며 실제로는 object(의 별칭)의 하위 유형입니다 System.Object. 사실, int기본 수업은 System.ValueType누가 기본 수업입니다 System.Object. 다음 표현식을 평가하여보십시오 typeof(int).BaseType.BaseType.. ReferenceEquals여기에서 false를 반환 하는 이유 는 int이 상자가 두 개의 별도 상자로 상자에 들어 있고 각 상자의 ID가 다른 상자와 다르기 때문입니다. 따라서 두 복싱 작업은 상자 값에 관계없이 항상 동일하지 않은 두 개의 객체를 생성합니다.
Allon Guralnek

@AllonGuralnek : 각 값 유형 (예 : System.Int32또는 List<String>.Enumerator)은 실제로 스토리지 위치 유형과 힙 객체 유형 ( "상자 형 값 유형"이라고도 함)의 두 가지 종류를 나타냅니다. 유형에서 파생 된 저장 위치 System.ValueType는 전자를 보유합니다. 유형이 같은 힙 오브젝트는 후자를 보유합니다. 대부분의 언어에서 후자는 전자에서 확대 캐스트가 존재하고 후자는 전자에서 확장 캐스트가 존재합니다. 박스형 값 유형은 값 유형 저장 위치와 동일한 유형 설명자를
갖지만

3

구현 세부 사항으로 귀결됩니다. 값 유형은 참조 유형과 다르게 구현됩니다.

값 유형을 참조 유형으로 처리하도록 강요하면 (예 : 인터페이스를 통해 참조 유형으로 상자에 표시) 분산을 얻을 수 있습니다.

차이점을 보는 가장 쉬운 방법은 단순히 다음을 고려하는 것입니다 Array. Value 유형의 배열은 연속적으로 (직접적으로) 메모리에 결합됩니다. 여기서 Reference 유형의 배열은 메모리에서 연속적으로 참조 (포인터) 만 갖습니다. 가리키는 객체는 별도로 할당됩니다.

다른 (관련) 문제 (*)는 (거의) 모든 참조 유형이 분산 목적으로 동일한 표현을 가지고 있으며 많은 코드가 유형 간의 차이점을 알 필요가 없으므로 공분산 및 역 분산이 가능하고 쉽게 종종 추가 유형 검사를 생략하여 구현됩니다.

(*) 같은 문제인 것 같습니다 ...

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.