C #의 배열은 어떻게 부분적으로 IList <T>를 구현합니까?


99

아시다시피 C #의 배열은 IList<T>다른 인터페이스 중에서를 구현 합니다. 하지만 어떻게 든 Count 속성을 공개적으로 구현하지 않고이 작업을 수행합니다 IList<T>. 배열에는 Length 속성 만 있습니다.

이것은 인터페이스 구현에 대한 자체 규칙을 위반하는 C # /. NET의 노골적인 예입니까? 아니면 뭔가 빠졌습니까?


2
아무도 Array클래스가 C #으로 작성되어야 한다고 말하지 않았습니다 !
user541686

ArrayC #이나 .net을 대상으로하는 다른 언어로는 구현할 수없는 "마법"클래스입니다. 그러나이 특정 기능은 C #에서 사용할 수 있습니다.
CodesInChaos

답변:


81

Hans의 답변에 비추어 새로운 답변

Hans의 답변 덕분에 구현이 생각보다 다소 복잡하다는 것을 알 수 있습니다. 컴파일러와 CLR 모두 배열 유형이 구현하는 인상을주기 위해 매우 열심히 노력 IList<T>하지만 배열 분산으로 인해이 작업이 더 까다로워집니다. 특정 배열의 유형이 있기 때문에 한스의 대답과는 달리, 배열 유형 (어쨌든, 제로, 하나의 차원은), 직접 제네릭 컬렉션을 구현합니까 하지 않습니다 System.Array 단지의 그 - 염기 배열의 유형입니다. 배열 유형에 지원하는 인터페이스를 묻는 경우 일반 유형이 포함됩니다.

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

산출:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

1 차원, 0 기반 배열의 경우 언어 에 관한 한 배열도 실제로 구현 IList<T>됩니다. C # 사양의 섹션 12.1.2는 그렇게 말합니다. 따라서 기본 구현이 무엇을하든, 언어는 다른 인터페이스와 마찬가지로 구현 유형이 작동 해야합니다 . 이러한 관점에서 인터페이스 명시 적으로 구현되는 일부 멤버 (예 :)로 구현됩니다 . 그것이 무슨 일이 일어나고 있는지에 대한 언어 수준 에서 가장 좋은 설명 입니다.T[]IList<T>Count

이것은 1 차원 배열 (및 0부터 시작하는 배열에만 적용되며, 언어로서의 C #이 0이 아닌 배열에 대해 아무 말도하지 않음)에만 적용됩니다. 구현 T[,] 하지 않습니다IList<T> .

CLR 관점에서 보면 좀 더 펑키 한 일이 벌어지고 있습니다. 일반 인터페이스 유형에 대한 인터페이스 매핑을 가져올 수 없습니다. 예를 들면 :

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

다음의 예외를 제공합니다.

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

그렇다면 왜 이상한가요? 글쎄요, 저는 그것이 실제로 배열 공분산 때문이라고 생각합니다. 이것은 타입 시스템 IMO에서 사마귀입니다. 비록 IList<T>입니다 하지 (안전하게 할 수 없다) 공변, 배열 공분산 작업이 할 수 있습니다 :

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... 이것은 실제로 그렇지 않은 경우 구현 처럼 보입니다 .typeof(string[])IList<object>

CLI 사양 (ECMA-335) 파티션 1, 섹션 8.7.1에는 다음이 포함됩니다.

서명 유형 T는 서명 유형 U와 호환되며 다음 중 하나 이상이 유지되는 경우에만 가능합니다.

...

T는 0부터 1 등급 배열 V[]하고 U있다 IList<W>, 그리고 V는 W.와 배열 요소 호환-IS

(실제로 사양에서 버그라고 생각 ICollection<W>하거나 언급하지 않습니다 IEnumerable<W>.)

비 변이의 경우 CLI 사양은 언어 사양과 직접 함께 진행됩니다. 파티션 1의 8.9.1 섹션에서 :

또한 요소 유형이 T 인 생성 된 벡터는 인터페이스를 구현합니다 System.Collections.Generic.IList<U>. 여기서 U : = T입니다. (§8.7)

( 벡터 는 밑이 0 인 1 차원 배열입니다.)

지금의 관점에서 구현 세부 사항 , 명확하게 CLR 여기에 할당 호환성을 유지하기 위해 몇 가지 펑키 매핑을하고있다 : A는 때 string[]의 이행을 요구한다 ICollection<object>.Count, 그것은에 그것을 처리 할 수없는 매우 일반적인 방법. 이것이 명시 적 인터페이스 구현으로 간주됩니까? 인터페이스 매핑을 직접 요청하지 않는 한 언어 관점에서 항상 그렇게 행동 하기 때문에 그렇게 처리하는 것이 합리적이라고 생각합니다 .

어때 ICollection.Count?

지금까지 제네릭 인터페이스에 대해 이야기했지만 속성이 있는 비 제네릭 ICollectionCount있습니다. 이번에 는 인터페이스 매핑을 얻을 있으며 실제로 인터페이스는 System.Array. 의 ICollection.Count속성 구현에 대한 설명서 Array에는 명시 적 인터페이스 구현으로 구현되었다고 명시되어 있습니다.

이런 종류의 명시 적 인터페이스 구현이 "일반적인"명시 적 인터페이스 구현과 다른 방식을 누구나 생각할 수 있다면 더 자세히 살펴 보겠습니다.

명시 적 인터페이스 구현에 대한 이전 답변

배열에 대한 지식으로 인해 더 복잡한 위의 내용에도 불구하고 명시 적 인터페이스 구현을 통해 동일한 가시적 효과로 작업을 수행 할 수 있습니다 .

다음은 간단한 독립 실행 형 예제입니다.

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}

5
foo.M1 ()에서 컴파일 타임 실패가 발생할 것이라고 생각합니다. 아니 foo.M2 ();
Kevin Aenmey 2012-06-22

여기서 문제는 배열과 같은 비 제네릭 클래스가 IList <>와 같은 제네릭 인터페이스 유형을 구현하도록하는 것입니다. 귀하의 스 니펫은 그렇게하지 않습니다.
Hans Passant 2012-06-22

@HansPassant : 제네릭이 아닌 클래스가 제네릭 인터페이스 유형을 구현하도록 만드는 것은 매우 쉽습니다. 하찮은. 나는 그것이 OP가 요구 한 것이라는 징후를 보지 못했습니다.
Jon Skeet

4
@JohnSaunders : 사실, 이전에 어떤 것도 부정확하다고 생각하지 않습니다. 나는 그것을 많이 확장하고 CLR이 배열을 이상하게 취급하는 이유를 설명했지만 명시 적 인터페이스 구현에 대한 내 대답은 이전에 꽤 정확하다고 생각합니다. 어떤면에서 동의하지 않습니까? 다시 말하지만, 세부 사항이 유용 할 것입니다 (해당되는 경우 자신의 답변에서 가능).
존 소총

1
@RBT : 예, 사용하는 Count것이 괜찮다 는 점에는 차이가 있지만 Add배열이 고정 크기이기 때문에 항상 발생합니다.
Jon Skeet

86

아시다시피 C #의 배열은 IList<T>다른 인터페이스 중에서를 구현합니다 .

글쎄, 네, 음, 아니에요. 다음은 .NET 4 프레임 워크의 Array 클래스에 대한 선언입니다.

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

System.Collections.Generic.IList <>가 아닌 System.Collections.IList를 구현 합니다. 할 수 없습니다. Array는 일반적이지 않습니다. 일반 IEnumerable <> 및 ICollection <> 인터페이스도 마찬가지입니다.

그러나 CLR은 구체적인 배열 유형을 즉석에서 생성하므로 기술적으로 이러한 인터페이스를 구현하는 배열 유형을 생성 할 수 있습니다. 그러나 이것은 사실이 아닙니다. 예를 들어 다음 코드를 시도하십시오.

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

"인터페이스를 찾을 수 없음"이있는 구체적인 배열 유형에 대해 GetInterfaceMap () 호출이 실패합니다. 그러나 IEnumerable <> 로의 캐스트는 문제없이 작동합니다.

이것은 꽥꽥 거리는 오리 같은 타이핑입니다. 모든 값 유형이 Object에서 파생 된 ValueType에서 파생된다는 착각을 일으키는 동일한 종류의 입력입니다. 컴파일러와 CLR은 모두 값 형식과 마찬가지로 배열 형식에 대한 특별한 지식을 가지고 있습니다. 컴파일러는 IList <>로 캐스팅하려는 사용자의 시도를보고 "좋아, 방법을 알고 있습니다!"라고 말합니다. 그리고 castclass IL 명령어를 내 보냅니다. CLR은 문제가 없으며 기본 배열 개체에서 작동하는 IList <> 구현을 제공하는 방법을 알고 있습니다. 실제로 이러한 인터페이스를 구현하는 래퍼 인 숨겨진 System.SZArrayHelper 클래스에 대한 지식이 내장되어 있습니다.

모든 사람의 주장과는 달리 명시 적으로 요청한 Count 속성은 다음과 같습니다.

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

네, 당신은 확실히 그 코멘트를 "규칙을 깨는 것"이라고 부를 수 있습니다. :) 그렇지 않으면 그것은 매우 편리합니다. 그리고 매우 잘 숨겨져 있으므로 CLR 용 공유 소스 배포 인 SSCLI20에서이를 확인할 수 있습니다. 유형 대체가 발생하는 위치를 보려면 "IList"를 검색하십시오. 실제 동작을 볼 수있는 가장 좋은 곳은 clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod () 메서드입니다.

CLR에서 이러한 종류의 대체는 WinRT (일명 Metro)에 대한 관리 코드를 작성할 수있는 CLR의 언어 프로젝션에서 발생하는 것과 비교할 때 매우 온화합니다. 거의 모든 핵심 .NET 유형이 여기에서 대체됩니다. IList <>는 예를 들어 완전히 관리되지 않는 형식 인 IVector <>에 매핑됩니다. 그 자체는 대체이며 COM은 제네릭 유형을 지원하지 않습니다.

글쎄, 그것은 커튼 뒤에서 일어나는 일을 보았습니다. 맵의 끝에 살고있는 용과 함께 매우 불편하고 이상하고 생소한 바다 일 수 있습니다. 지구를 평평하게 만들고 관리 코드에서 실제로 일어나는 일에 대한 다른 이미지를 모델링하는 것은 매우 유용 할 수 있습니다. 모두가 좋아하는 답변에 매핑하는 것은 그렇게 편안합니다. 값 유형에 대해 잘 작동하지 않지만 (구조체를 변경하지 마십시오!) 이것은 매우 잘 숨겨져 있습니다. GetInterfaceMap () 메서드 실패는 내가 생각할 수있는 추상화의 유일한 누수입니다.


1
이것은 배열의 유형 이 아닌Array 클래스에 대한 선언입니다 . 배열 의 기본 유형입니다. C #에서 일차원 배열 않는 구현 . 그리고 비 제네릭 형식은 확실히 많이 있기 때문에 작동 어쨌든 일반적인 인터페이스 ... 구현할 수있는 다른 - 유형 ! = 대해서 typeof (문자열 []) 대해서 typeof (INT [])`구현 및 구현을 . IList<T>typeof(int[]), so IList<int>typeof(string[])IList<string>
존 소총

2
@HansPassant : 내가 불안한 이유 때문에 무언가를 반대 할 것이라고 생각하지 마십시오 . 사실은 당신의 추론 Array(당신이 보여준 것처럼 추상 클래스이므로 배열 객체 의 실제 유형 이 될 수 없음 )과 결론 (구현하지 않는다는 것 IList<T>)은 모두 잘못된 IMO입니다. 방법 이 구현되는가 IList<T>, 독특하고 흥미 동의합니다 -하지만 그건 순전히의 구현 세부 사항. T[]그것이 구현되지 않는다고 주장하는 것은 IList<T>IMO를 오도 하는 것입니다 . 사양 및 관찰 된 모든 동작에 위배됩니다.
Jon Skeet

6
글쎄, 당신은 그것이 틀렸다고 생각합니다. 사양에서 읽은 내용과 일치하도록 만들 수 없습니다. 당신의 방식대로 보아 주시기 바랍니다. 그러나 GetInterfaceMap ()이 실패한 이유에 대한 좋은 설명은 결코 나오지 않을 것입니다. "뭔가 펑키"는 그다지 통찰이 아닙니다. 나는 구현 용 안경을 쓰고있다. 물론 실패하고, 돌팔이와 같은 타이핑이고, 구체적인 배열 유형은 실제로 ICollection <>을 구현하지 않는다. 그것에 대해 펑키 한 것은 없습니다. 여기에 보관합시다. 우리는 절대 동의하지 않을 것입니다.
Hans Passant 2012-06-23

4
무엇 적어도 주장 배열 구현할 수없는 가짜 논리를 제거하는 방법에 대한 IList<T> 때문에 Array 하지 않습니다? 그 논리는 내가 동의하지 않는 것의 큰 부분입니다. 그건 그렇고, 우리가이 인터페이스를 구현하는 유형에 대해 무엇을 의미하는지의 정의에 동의 할 거라고 생각 : 내 마음에, 배열 유형을 표시 모두를 관찰 할 수있는 기능을 구현하는 유형의 IList<T>이외, GetInterfaceMapping. 다시 말하지만, 구현 세부 사항 이 다르 System.String더라도 그것이 불변 이라고 말하는 것이 괜찮은 것처럼 그것이 달성되는 방법은 나에게 덜 중요합니다 .
Jon Skeet

1
C ++ CLI 컴파일러는 어떻습니까? 그 사람은 분명히 "나는 그것을하는 방법을 모른다!"라고 말합니다. 오류가 발생합니다. IList<T>작동하려면 명시 적 캐스트가 필요합니다 .
Tobias Knauss

21

IList<T>.Count명시 적으로 구현됩니다 .

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

이것은 간단한 배열 변수가있을 때 둘 다 사용할 수 Count없고 Length직접 사용할 수 없도록 수행 됩니다.

일반적으로 명시 적 인터페이스 구현은 유형의 모든 소비자가 유형에 대해 그렇게 생각하지 않고 특정 방식으로 유형을 사용할 수 있는지 확인하려는 경우에 사용됩니다.

편집 : 죄송합니다 . ICollection.Count명시 적으로 구현됩니다. 제네릭 IList<T>아래의 Hans descibes 로 처리됩니다 .


4
하지만 왜 그들이 Length 대신 Count 속성을 호출하지 않았는지 궁금합니다. Array는 이러한 속성을 가진 유일한 공통 컬렉션입니다 (를 계산하지 않는 한 string).
Tim S.

5
@TimS 좋은 질문 (그리고 내가 모르는 대답.) 나는 그 이유가 "count"가 몇 개의 항목을 의미하기 때문이라고 추측 할 것입니다. 반면 배열은 할당 되 자마자 변경할 수없는 "길이"를 갖기 때문입니다. 값이있는 요소에 관계없이.)
dlev

1
@TimS 나는 그것이 ICollection선언 하기 때문에 완료되었다고 생각하며 Count, "컬렉션"이라는 단어가 포함 된 유형이 :)를 사용하지 않으면 훨씬 혼란 스러울 것 Count입니다. 이러한 결정을 내리는 데는 항상 상충 관계가 있습니다.
dlev

4
@JohnSaunders : 그리고 다시 ... 유용한 정보 가 없는 반대 투표 입니다.
존 소총

5
@JohnSaunders : 나는 아직도 확신하지 못한다. Hans는 SSCLI 구현을 언급했지만 IList<T>, 언어와 CLI 사양이 모두 반대로 보임 에도 불구하고 배열 유형이 구현하지 않는다고 주장했습니다 . 인터페이스 구현이 내부적으로 작동하는 방식은 복잡 할 수 있지만 많은 상황에서 그렇습니다. 내부 작업이 변경 가능 System.String하기 때문에 변경 불가능 하다고 말하는 사람에게도 반대 투표를 하시겠습니까 ? 들어 모든 실제적인 목적 - 확실히 멀리 C # 언어에 관한 한 - 그것 입니다 명시 적 IMPL.
Jon Skeet


2

IList의 명시 적 인터페이스 구현과 다르지 않습니다. 인터페이스를 구현한다고해서 해당 멤버가 클래스 멤버로 나타나야하는 것은 아닙니다. 그것은 하지 Count 속성을 구현, 그냥 X []에 노출되지 않습니다.


1

참조 소스 사용 가능 :

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

특히이 부분 :

인터페이스 스텁 디스패처 는이를 특수한 경우로 처리 하고 SZArrayHelper를로드 하고 해당하는 제네릭 메서드 ( 메서드 이름으로 간단하게 일치)를 찾고 유형에 대해 인스턴스화하고 실행합니다.

(강조 광산)

소스 (위로 스크롤).

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