Como matrizes em C # implementam parcialmente IList <T>?

99

Como você deve saber, arrays em C # implementam IList<T>, entre outras interfaces. Porém, de alguma forma, eles fazem isso sem implementar publicamente a propriedade Count de IList<T>! Matrizes têm apenas uma propriedade de comprimento.

Este é um exemplo flagrante de C # / .net quebrando suas próprias regras sobre a implementação da interface ou estou faltando alguma coisa?

MgSam
fonte
2
Ninguém disse que a Arrayaula tinha que ser escrita em C #!
user541686
Arrayé uma classe "mágica" que não poderia ser implementada em C # ou em qualquer outro idioma de destino .net. Mas esse recurso específico está disponível em C #.
CodesInChaos

Respostas:

81

Nova resposta à luz da resposta de Hans

Graças à resposta dada por Hans, podemos ver que a implementação é um pouco mais complicada do que podemos imaginar. Tanto o compilador quanto o CLR tentam muito dar a impressão de que um tipo de array implementa IList<T>- mas a variação de array torna isso mais complicado. Ao contrário da resposta de Hans, os tipos de array (unidimensional, baseado em zero de qualquer maneira) implementam as coleções genéricas diretamente, porque o tipo de qualquer array específico não é System.Array - esse é apenas o tipo base do array. Se você perguntar a um tipo de array quais interfaces ele suporta, ele incluirá os tipos genéricos:

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

Resultado:

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]

Para matrizes unidimensionais baseadas em zero, no que diz respeito à linguagem , a matriz IList<T>também implementa . A seção 12.1.2 da especificação C # diz isso. Portanto, seja o que for que a implementação subjacente faça, a linguagem deve se comportar como se o tipo de T[]implemento fosse feito IList<T>com qualquer outra interface. Desta perspectiva, a interface é implementada com alguns dos membros sendo explicitamente implementados (como Count). Essa é a melhor explicação em nível de linguagem para o que está acontecendo.

Observe que isso só é válido para arrays unidimensionais (e arrays baseados em zero, não que o C # como linguagem diga algo sobre arrays não baseados em zero). T[,] não implementa IList<T>.

De uma perspectiva CLR, algo mais funk está acontecendo. Você não pode obter o mapeamento de interface para os tipos de interface genéricos. Por exemplo:

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

Oferece uma exceção de:

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

Então, por que a esquisitice? Bem, eu acredito que é realmente devido à covariância de array, que é uma verruga no sistema de tipos, IMO. Mesmo que nãoIList<T> seja covariante (e não pode ser seguro), a covariância de matriz permite que isso funcione:

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

... o que torna olhar como typeof(string[])implementos IList<object>, quando isso não acontece realmente.

A especificação CLI (ECMA-335) partição 1, seção 8.7.1, tem o seguinte:

Um tipo de assinatura T é compatível com um tipo de assinatura U se e somente se pelo menos um dos seguintes for válido

...

T é uma matriz de classificação 1 baseada em zero V[]e Ué IList<W>, e V é compatível com elemento de matriz com W.

(Na verdade, não menciona ICollection<W>ou IEnumerable<W>acredito que seja um bug na especificação.)

Para não variação, a especificação CLI acompanha a especificação do idioma diretamente. Da seção 8.9.1 da partição 1:

Além disso, um vetor criado com o tipo de elemento T, implementa a interface System.Collections.Generic.IList<U>, onde U: = T. (§8.7)

(Um vetor é uma matriz unidimensional com base zero.)

Agora em termos dos detalhes de implementação , claramente o CLR está fazendo algum mapeamento descolados para manter a compatibilidade atribuição aqui: quando um string[]é convidado para a implementação ICollection<object>.Count, não pode lidar com isso em muito a maneira normal. Isso conta como implementação de interface explícita? Acho que é razoável tratá-lo dessa forma, já que, a menos que você peça o mapeamento da interface diretamente, ele sempre se comportará dessa forma do ponto de vista da linguagem.

Sobre o quê ICollection.Count?

Até agora, falei sobre as interfaces genéricas, mas há as não genéricas ICollectioncom sua Countpropriedade. Desta vez, podemos obter o mapeamento da interface e, de fato, a interface é implementada diretamente por System.Array. A documentação para a ICollection.Countimplementação da propriedade Arrayindica que é implementada com a implementação explícita da interface.

Se alguém conseguir pensar em uma maneira em que esse tipo de implementação de interface explícita seja diferente da implementação de interface explícita "normal", ficaria feliz em investigar mais a fundo.

Resposta antiga sobre implementação de interface explícita

Apesar do acima, que é mais complicado por causa do conhecimento de matrizes, você ainda pode fazer algo com os mesmos efeitos visíveis por meio da implementação de interface explícita .

Aqui está um exemplo autônomo simples:

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
    }
}
Jon Skeet
fonte
5
Eu acho que você terá uma falha de tempo de compilação em foo.M1 (); não foo.M2 ();
Kevin Aenmey
O desafio aqui é fazer com que uma classe não genérica, como um array, implemente um tipo de interface genérico, como IList <>. Seu snippet não faz isso.
Hans Passant
@HansPassant: É muito fácil fazer uma classe não genérica implementar um tipo de interface genérico. Trivial. Não vejo qualquer indicação de que seja isso que o OP estava perguntando.
Jon Skeet
4
@JohnSaunders: Na verdade, não acredito que nada disso estivesse incorreto antes. Eu a expandi muito e expliquei por que o CLR trata os arrays de maneira estranha - mas acredito que minha resposta sobre a implementação explícita da interface estava bem correta antes. De que forma você discorda? Novamente, os detalhes seriam úteis (possivelmente em sua própria resposta, se apropriado).
Jon Skeet
1
@RBT: Sim, embora haja uma diferença em que usar Counté bom - mas Addsempre lançará, já que os arrays têm tamanho fixo.
Jon Skeet
86

Como você deve saber, arrays em C # implementam IList<T>, entre outras interfaces

Bem, sim, erm não, realmente não. Esta é a declaração para a classe Array na estrutura .NET 4:

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

Ele implementa System.Collections.IList, não System.Collections.Generic.IList <>. Não pode, Array não é genérico. O mesmo vale para as interfaces genéricas IEnumerable <> e ICollection <>.

Mas o CLR cria tipos de array concretos dinamicamente, então ele poderia criar tecnicamente um que implemente essas interfaces. No entanto, este não é o caso. Experimente este código, por exemplo:

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(); }
}

A chamada GetInterfaceMap () falha para um tipo de array concreto com "Interface não encontrada". No entanto, uma conversão para IEnumerable <> funciona sem problemas.

Esta é a digitação de quacks-like-a-duck. É o mesmo tipo de digitação que cria a ilusão de que todo tipo de valor deriva de ValueType que deriva de Object. Tanto o compilador quanto o CLR têm conhecimento especial dos tipos de array, assim como têm dos tipos de valor. O compilador vê sua tentativa de lançar para IList <> e diz "ok, eu sei como fazer isso!". E emite a instrução castclass IL. O CLR não tem problemas com isso, ele sabe como fornecer uma implementação de IList <> que funciona no objeto array subjacente. Ele possui conhecimento integrado da classe System.SZArrayHelper, de outra forma oculta, um wrapper que realmente implementa essas interfaces.

O que não acontece explicitamente como todos afirmam, a propriedade Count sobre a qual você perguntou se parece com isto:

    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;
    }

Sim, certamente você pode chamar esse comentário de "quebrar as regras" :) De outra forma, é muito útil. E extremamente bem escondido, você pode verificar isso em SSCLI20, a distribuição de código-fonte compartilhado para o CLR. Procure por "IList" para ver onde ocorre a substituição de tipo. O melhor lugar para vê-lo em ação é o método clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

Este tipo de substituição no CLR é bastante brando em comparação com o que acontece na projeção da linguagem no CLR, que permite escrever código gerenciado para WinRT (também conhecido como Metro). Quase qualquer tipo de núcleo .NET é substituído lá. IList <> mapeia para IVector <> por exemplo, um tipo totalmente não gerenciado. Sendo uma substituição, o COM não oferece suporte a tipos genéricos.

Bem, essa foi uma olhada no que acontece por trás da cortina. Pode ser muito desconfortável, mares estranhos e desconhecidos com dragões que vivem no final do mapa. Pode ser muito útil tornar a Terra plana e modelar uma imagem diferente do que realmente está acontecendo no código gerenciado. Mapear para a resposta favorita de todos é confortável dessa forma. O que não funciona tão bem para tipos de valor (não modifique uma estrutura!), Mas este está muito bem escondido. A falha do método GetInterfaceMap () é o único vazamento na abstração que posso pensar.

Hans Passant
fonte
1
Essa é a declaração da Arrayclasse, que não é o tipo de um array. É o tipo base para um array. Uma matriz unidimensional em C # O implementar IList<T>. E um tipo não genérico pode certamente implementar uma interface genérica de qualquer maneira ... o que funciona porque há muitos tipos diferentes - typeof(int[])! = Typeof (string []) , so typeof (int []) `implementa IList<int>e typeof(string[])implementa IList<string>.
Jon Skeet
2
@HansPassant: Por favor, não presuma que eu rejeitei algo apenas por ser perturbador . O fato é que tanto o seu raciocínio via Array(que, como você mostra, é uma classe abstrata, portanto, não pode ser o tipo real de um objeto de matriz) e a conclusão (que ele não implementa IList<T>) são IMO incorretos. A maneira como ele implementa IList<T>é incomum e interessante, eu concordo - mas isso é puramente um detalhe de implementação . Afirmar que T[]não implementa IList<T>é enganoso IMO. Isso vai contra as especificações e todos os comportamentos observados.
Jon Skeet
6
Bem, claro que você acha que está incorreto. Você não pode fazer coincidir com o que você lê nas especificações. Sinta-se à vontade para ver do seu jeito, mas você nunca vai encontrar uma boa explicação do porque GetInterfaceMap () falha. "Algo funky" não é muito um insight. Estou usando óculos de implementação: claro que falha, é uma tipificação charlatã-como-um-pato, um tipo de array concreto não implementa ICollection <>. Não há nada de estranho nisso. Vamos mantê-lo aqui, nunca vamos concordar.
Hans Passant
4
Que tal, pelo menos, remover a lógica espúria que afirma que os arrays não podem implementar IList<T> porque Array não o fazem? Essa lógica é uma grande parte do que estou discordando. Além disso, acho que teríamos que concordar sobre uma definição do que significa para um tipo implementar uma interface: na minha opinião, os tipos de array exibem todos os recursos observáveis de tipos que implementam IList<T>, exceto GetInterfaceMapping. Novamente, como isso é alcançado é menos importante para mim, assim como não vejo problema em dizer que System.Stringé imutável, embora os detalhes de implementação sejam diferentes.
Jon Skeet
1
E quanto ao compilador C ++ CLI? Aquele obviamente diz "Não tenho ideia de como fazer isso!" e emite um erro. Ele precisa de um elenco explícito IList<T>para funcionar.
Tobias Knauss
21

IList<T>.Counté implementado explicitamente :

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

Isso é feito para que, quando você tiver uma variável de array simples, não tenha ambos Counte Lengthdiretamente disponíveis.

Em geral, a implementação de interface explícita é usada quando você deseja garantir que um tipo possa ser usado de uma maneira particular, sem forçar todos os consumidores do tipo a pensar sobre isso dessa maneira.

Edit : Opa, lembrança ruim aí. ICollection.Counté implementado explicitamente. O genérico IList<T>é tratado como Hans descreve abaixo .

dlev
fonte
4
Mas me faz pensar por que eles simplesmente não chamaram a propriedade de Count em vez de Comprimento? Array é a única coleção comum que possui tal propriedade (a menos que você conte string).
Tim S.
5
@TimS Uma boa pergunta (e cuja resposta eu não sei.) Eu especularia que o motivo é porque "contagem" implica um certo número de itens, enquanto um array tem um "comprimento" imutável assim que é alocado ( independentemente de quais elementos têm valores.)
dlev
1
@TimS Acho que isso é feito porque ICollectiondeclara Count, e ficaria ainda mais confuso se um tipo com a palavra "coleção" não usasse Count:). Sempre há compensações ao se tomar essas decisões.
dlev
4
@JohnSaunders: E novamente ... apenas um downvote sem nenhuma informação útil.
Jon Skeet
5
@JohnSaunders: Ainda não estou convencido. Hans se referiu à implementação SSCLI, mas também afirmou que os tipos de array nem mesmo implementam IList<T>, apesar de as especificações de linguagem e CLI parecerem o contrário. Ouso dizer que a maneira como a implementação da interface funciona nos bastidores pode ser complicada, mas é o caso em muitas situações. Você também votaria negativamente em alguém dizendo que isso System.Stringé imutável, só porque o funcionamento interno é mutável? Para todos os efeitos práticos - e certamente no que diz respeito à linguagem C # - é implícito explícito.
Jon Skeet
2

Não é diferente de uma implementação de interface explícita de IList. Só porque você implementa a interface não significa que seus membros precisam aparecer como membros da classe. Ele faz implementar a propriedade Count, ele simplesmente não expô-lo em X [].

Nitzmahone
fonte
1

Com fontes de referência disponíveis:

//----------------------------------------------------------------------------------------
// ! 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 ... */
}

Especificamente esta parte:

o despachante de stub da interface trata isso como um caso especial , carrega SZArrayHelper, encontra o método genérico correspondente (correspondido simplesmente pelo nome do método) , instancia-o para o tipo e o executa.

(Ênfase minha)

Fonte (role para cima).

AnorZaken
fonte