É uma boa prática herdar de tipos genéricos?


35

É melhor usar List<string>em anotações de tipo ou StringListondeStringList

class StringList : List<String> { /* no further code!*/ }

Corri para vários de estes na ironia .


Se você não herda de tipos genéricos, por que eles estão lá?
MAWG

7
@Mawg Eles podem estar disponíveis apenas para instanciação.
Theodoros Chatzigiannakis

3
Este é um dos meus menos coisas favoritas para ver no código
Kik

4
Que nojo. Não, sério. Qual é a diferença semântica entre StringListe List<string>? Não existe, mas você (o genérico "você") conseguiu adicionar outro detalhe à base de código que é exclusiva apenas para o seu projeto e não significa nada para ninguém até que eles tenham estudado o código. Por que mascarar o nome de uma lista de strings apenas para continuar chamando de lista de strings? Por outro lado, se você quisesse, BagOBagels : List<string>acho que faz mais sentido. Você pode iterá-lo como IEnumerable, consumidores externos não se importam com a implementação - bondade em todos os aspectos.
Craig

11
XAML tem um problema em que você não pode usar os genéricos e você necessário para fazer um tipo como este para preencher uma lista
Daniel Little

Respostas:


27

Há outra razão pela qual você pode herdar de um tipo genérico.

A Microsoft recomenda evitar o aninhamento de tipos genéricos nas assinaturas de método .

É uma boa ideia, então, criar tipos nomeados no domínio comercial para alguns de seus genéricos. Em vez de ter um IEnumerable<IDictionary<string, MyClass>>, crie um tipo MyDictionary : IDictionary<string, MyClass>e seu membro se tornará um IEnumerable<MyDictionary>, o que é muito mais legível. Ele também permite que você use o MyDictionary em vários locais, aumentando a explicitação do seu código.

Isso pode não parecer um grande benefício, mas no código comercial do mundo real eu vi genéricos que farão seu cabelo arrepiar. Coisas como List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. O significado desse genérico não é de forma alguma óbvio. No entanto, se fosse construído com uma série de tipos criados explicitamente, a intenção do desenvolvedor original provavelmente seria muito mais clara. Além disso, se esse tipo genérico precisou ser alterado por algum motivo, encontrar e substituir todas as instâncias desse tipo genérico pode ser um problema, comparado com a alteração na classe derivada.

Finalmente, há outra razão pela qual alguém pode querer criar seus próprios tipos derivados de genéricos. Considere as seguintes classes:

public class MyClass
{
    public ICollection<MyItems> MyItems { get; private set; }
    // plumbing code here
}

public class MyOtherClass
{
    public ICollection<MyItems> MyItemCache { get; private set; }
    // plumbing code here
}

public class MyConsumerClass
{
    public MyConsumerClass(ICollection<MyItems> myItems)
    {
        // use the collection
    }
}

Agora, como sabemos qual ICollection<MyItems>passar para o construtor para MyConsumerClass? Se criarmos classes derivadas class MyItems : ICollection<MyItems>e class MyItemCache : ICollection<MyItems>, MyConsumerClass poderá ser extremamente específico sobre qual das duas coleções ela realmente deseja. Isso funciona muito bem com um contêiner de IoC, que pode resolver as duas coleções com muita facilidade. Ele também permite que o programador seja mais preciso - se faz sentido MyConsumerClasstrabalhar com qualquer ICollection, eles podem usar o construtor genérico. Se, no entanto, nunca fizer sentido comercial usar um dos dois tipos derivados, o desenvolvedor poderá restringir o parâmetro do construtor ao tipo específico.

Em resumo, geralmente vale a pena derivar tipos de tipos genéricos - ele permite um código mais explícito, quando apropriado, e ajuda na legibilidade e manutenção.


12
Essa é uma recomendação absolutamente ridícula da Microsoft.
22614 Thomas Eding

7
Não acho isso ridículo para APIs públicas. Os genéricos aninhados são difíceis de entender rapidamente e um nome de tipo mais significativo geralmente pode ajudar na legibilidade.
Stephen

4
Eu não acho isso ridículo, e certamente é válido para APIs privadas ... mas para APIs públicas, não acho que seja uma boa ideia. Até a Microsoft recomenda aceitar e retornar os tipos mais básicos ao escrever APIs destinadas ao consumo público.
Paul d'Aoust

35

Em princípio, não há diferença entre herdar de tipos genéricos e herdar de qualquer outra coisa.

No entanto, por que diabos você derivaria de qualquer classe e não acrescentaria mais nada?


17
Concordo. Sem contribuir com nada, eu li "StringList" e pensei comigo mesmo: "O que isso realmente faz?"
Neil

11
Também concordo que, em alguns idiomas para herdar você use a palavra-chave "extends", este é um bom lembrete de que, se você deseja herdar uma classe, deve estendê-la com mais funcionalidades.
Elliot Blackburn

14
-1, por não entender que essa é apenas uma maneira de criar uma abstração escolhendo um nome diferente - criar uma abstração pode ser uma boa razão para não adicionar nada a essa classe.
Doc Brown

11
Considere a minha resposta, abordarei algumas das razões pelas quais alguém pode decidir fazer isso.
NPSF3000

11
"Por que diabos você derivaria de alguma classe e não acrescentaria mais nada?" Eu fiz isso para controles de usuário. Você pode criar um controle de usuário genérico, mas não pode usá-lo em um designer de formulário do Windows; portanto, é necessário herdá-lo, especificando o tipo e usá-lo.
George T

13

Não conheço muito bem o C #, mas acredito que esta é a única maneira de implementar um typedef, que os programadores C ++ geralmente usam por alguns motivos:

  • Isso evita que seu código pareça um mar de colchetes angulares.
  • Ele permite que você troque diferentes contêineres subjacentes se suas necessidades mudarem mais tarde e deixa claro que você se reserva o direito de fazê-lo.
  • Permite atribuir um nome a um tipo que seja mais relevante no contexto.

Eles basicamente jogaram fora a última vantagem ao escolher nomes genéricos e chatos, mas os outros dois motivos ainda permanecem.


7
Eh ... Eles não são exatamente iguais. No C ++, você tem um alias literal. StringList é um List<string>- eles podem ser usados ​​de forma intercambiável. Isso é importante ao trabalhar com outras APIs (ou ao expor sua API), pois todos os outros no mundo usam List<string>.
Telastyn

2
Sei que não são exatamente a mesma coisa. Tão perto quanto estiver disponível. Certamente existem desvantagens na versão mais fraca.
Karl Bielefeldt

24
Não, using StringList = List<string>;é o equivalente C # mais próximo de um typedef. Subclassificar assim impedirá o uso de qualquer construtor com argumentos.
Pete Kirkham

4
@PeteKirkham: a definição de uma StringList usingrestringe-a ao arquivo de código-fonte onde usingestá colocado. Deste ponto de vista, a herança está mais próxima typedef. E criar uma subclasse não impede que alguém adicione todos os construtores necessários (embora isso possa ser entediante).
Doc Brown

2
@ Random832: deixe-me expressar isso de maneira diferente: em C ++, um typedef pode ser usado para atribuir o nome em um único local , para torná-lo uma "única fonte de verdade". Em C #, usingnão pode ser usado para esse fim, mas a herança pode. Isso mostra por que eu discordo do comentário votado de Pete Kirkham - não considero usinguma alternativa real aos C ++ typedef.
Doc Brown

9

Eu posso pensar em pelo menos uma razão prática.

Declarar tipos não genéricos que herdam de tipos genéricos (mesmo sem adicionar nada) permite que esses tipos sejam referenciados a partir de locais onde eles não estariam disponíveis ou disponíveis de uma maneira mais complicada do que deveriam.

Um desses casos é ao adicionar configurações através do editor de Configurações no Visual Studio, onde apenas tipos não genéricos parecem estar disponíveis. Se você for lá, notará que todas as System.Collectionsclasses estão disponíveis, mas nenhuma coleção é System.Collections.Genericexibida conforme aplicável.

Outro caso é ao instanciar tipos por meio de XAML no WPF. Não há suporte para a passagem de argumentos de tipo na sintaxe XML ao usar um esquema anterior a 2009. Isso é agravado pelo fato de que o esquema de 2006 parece ser o padrão para novos projetos WPF.


11
Em interfaces de serviços web WCF você pode usar esta técnica para proporcionar uma melhor procura nomes tipo de coleção (por exemplo PersonCollection vez de ArrayOfPerson ou qualquer outra coisa.)
Rico Suter

7

Eu acho que você precisa investigar um pouco da história .

  • O C # é usado há muito mais tempo do que os tipos genéricos.
  • Espero que o software fornecido tenha seu próprio StringList em algum momento da história.
  • Isso pode ter sido implementado envolvendo uma ArrayList.
  • Então alguém a refatorar para avançar a base de código.
  • Mas não desejou tocar em 1001 arquivos diferentes, então termine o trabalho.

Caso contrário, Theodoros Chatzigiannakis teve as melhores respostas, por exemplo, ainda existem muitas ferramentas que não entendem tipos genéricos. Ou talvez até uma mistura de sua resposta e história.


3
"O C # é usado há muito mais tempo do que os tipos genéricos." Na verdade, o C # sem genéricos existe há cerca de 4 anos (janeiro de 2002 a novembro de 2005), enquanto o C # com genéricos agora tem 9 anos.
svick


4

Em alguns casos, é impossível usar tipos genéricos, por exemplo, você não pode fazer referência a um tipo genérico no XAML. Nesses casos, você pode criar um tipo não genérico que herda do tipo genérico que originalmente queria usar.

Se possível, eu evitaria isso.

Ao contrário de um typedef, você cria um novo tipo. Se você usar o StringList como um parâmetro de entrada para um método, seus chamadores não poderão passar a List<string>.

Além disso, você precisaria copiar explicitamente todos os construtores que deseja usar, no exemplo StringList que você não poderia dizer new StringList(new[]{"a", "b", "c"}), mesmo que List<T>defina esse construtor.

Editar:

A resposta aceita se concentra nos profissionais ao usar os tipos derivados dentro do seu modelo de domínio. Eu concordo que eles possam ser úteis nesse caso, ainda assim, eu pessoalmente os evitaria mesmo lá.

Mas você deve (na minha opinião) nunca usá-los em uma API pública porque você dificulta a vida de quem chama ou perde desempenho (também não gosto de conversões implícitas em geral).


3
Você diz: " você não pode fazer referência a um tipo genérico em XAML ", mas não sei ao que você está se referindo. Veja Genéricos em XAML
pswg

11
Foi concebido como um exemplo. Sim, é possível com o esquema XAML de 2009, mas o AFAIK não com o esquema de 2006, que ainda é o padrão do VS.
Lukas Rieger

11
Seu terceiro parágrafo é apenas meia verdade, você pode realmente permitir isso com conversores implícitas;)
Knerd

11
@ Knerd, true, mas você cria 'implicitamente' um novo objeto. A menos que você faça muito mais trabalho, isso também criará uma cópia dos dados. Provavelmente não importa com pequenas listas, mas se divertir, se alguém passa uma lista com alguns milhares de elementos =)
Lukas Rieger

@LukasRieger você está certo: PI só queria salientar isso: P
Knerd

2

Para o que vale, aqui está um exemplo de como a herança de uma classe genérica é usada no compilador Roslyn da Microsoft e sem alterar o nome da classe. (Fiquei tão confuso com isso que acabei aqui na minha pesquisa para ver se isso era realmente possível.)

No projeto CodeAnalysis, você pode encontrar esta definição:

/// <summary>
/// Common base class for C# and VB PE module builder.
/// </summary>
internal abstract class PEModuleBuilder<TCompilation, TSourceModuleSymbol, TAssemblySymbol, TTypeSymbol, TNamedTypeSymbol, TMethodSymbol, TSyntaxNode, TEmbeddedTypesManager, TModuleCompilationState> : CommonPEModuleBuilder, ITokenDeferral
    where TCompilation : Compilation
    where TSourceModuleSymbol : class, IModuleSymbol
    where TAssemblySymbol : class, IAssemblySymbol
    where TTypeSymbol : class
    where TNamedTypeSymbol : class, TTypeSymbol, Cci.INamespaceTypeDefinition
    where TMethodSymbol : class, Cci.IMethodDefinition
    where TSyntaxNode : SyntaxNode
    where TEmbeddedTypesManager : CommonEmbeddedTypesManager
    where TModuleCompilationState : ModuleCompilationState<TNamedTypeSymbol, TMethodSymbol>
{
  ...
}

Então, no projeto CSharpCodeanalysis, há esta definição:

internal abstract class PEModuleBuilder : PEModuleBuilder<CSharpCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState>
{
  ...
}

Essa classe PEModuleBuilder não genérica é usada extensivamente no projeto CSharpCodeanalysis e várias classes nesse projeto são herdadas, direta ou indiretamente.

E então, no projeto BasicCodeanalysis, há esta definição:

Partial Friend MustInherit Class PEModuleBuilder
    Inherits PEModuleBuilder(Of VisualBasicCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState)

Como podemos (espero) supor que Roslyn foi escrita por pessoas com amplo conhecimento de C # e como deve ser usada, estou pensando que essa é uma recomendação da técnica.


Sim. E também podem ser usados ​​para refinar a cláusula where diretamente quando declarada.
Sentinel

1

Existem três razões possíveis pelas quais alguém tentaria fazer isso em cima da minha cabeça:

1) Para simplificar as declarações:

Claro StringListe ListStringtêm o mesmo comprimento, mas imagine que você esteja trabalhando com um exemplo UserCollectionreal Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>ou outro grande exemplo genérico.

2) Para ajudar a AOT:

Às vezes, você precisa usar a compilação Ahead Of Time, não a Just In Time Compilation - por exemplo, desenvolvendo para iOS. Às vezes, o compilador AOT tem dificuldade em descobrir todos os tipos genéricos com antecedência, e isso pode ser uma tentativa de fornecer uma dica.

3) Para adicionar funcionalidade ou remover dependência:

Talvez eles tenham funcionalidades específicas para as quais desejam implementar StringList(podem estar ocultos em um método de extensão, ainda não foram adicionados ou históricos) aos quais não podem ser adicionadosList<string> sem herdar dele ou que não desejam poluir List<string>com .

Como alternativa, eles podem alterar a implementação da StringListtrilha, portanto, apenas usaram a classe como marcador por enquanto (normalmente você faria / usaria interfaces para isso, por exemplo IList).


Eu também observaria que você encontrou esse código no Irony, que é um analisador de idiomas - um tipo de aplicativo bastante incomum. Pode haver algum outro motivo específico pelo qual isso foi usado.

Esses exemplos demonstram que pode haver razões legítimas para escrever tal declaração - mesmo que não seja muito comum. Como em qualquer coisa, considere várias opções e escolha qual é a melhor para você no momento.


Se você der uma olhada no código - fonte , parece que a opção 3 é o motivo - eles usam esta técnica para ajudar a adicionar funcionalidades / especializar coleções:

public class StringList : List<string> {
    public StringList() { }
    public StringList(params string[] args) {
      AddRange(args);
    }
    public override string ToString() {
      return ToString(" ");
    }
    public string ToString(string separator) {
      return Strings.JoinStrings(separator, this);
    }
    //Used in sorting suffixes and prefixes; longer strings must come first in sort order
    public static int LongerFirst(string x, string y) {
      try {//in case any of them is null
        if (x.Length > y.Length) return -1;
      } catch { }
      if (x == y) return 0;
      return 1; 
    }

11
Às vezes, a busca de simplificar as declarações (ou simplificar o que quer que seja) pode resultar em maior complexidade, em vez de complexidade reduzida. Todo mundo que conhece moderadamente a língua sabe o que List<T>é, mas precisa inspecionar StringListno contexto para realmente entender o que é. Na realidade, é apenas List<string>, usando um nome diferente. Realmente não parece haver nenhuma vantagem semântica em fazer isso em um caso tão simples. Na verdade, você está apenas substituindo um tipo conhecido pelo mesmo tipo, com um nome diferente.
Craig

@ Craig Eu concordo, conforme observado pela minha explicação de caso de uso (declarações mais longas) e outros possíveis motivos.
NPSF3000

-1

Eu diria que não é uma boa prática.

List<string>comunica "uma lista genérica de strings". Claramente, os List<string> métodos de extensão se aplicam. Menos complexidade é necessária para entender; somente a lista é necessária.

StringListcomunica "a necessidade de uma classe separada". Talvez os List<string> métodos de extensão se apliquem (verifique a implementação do tipo real para isso). É necessária mais complexidade para entender o código; A lista e o conhecimento de implementação da classe derivada são necessários.


4
Os métodos de extensão se aplicam de qualquer maneira.
Theodoros Chatzigiannakis

2
Claro. Mas você não sabe que até inspecionar o StringList e ver que ele deriva List<string>.
Ruudjah

@TheodorosChatzigiannakis Gostaria de saber se Ruudjah poderia elaborar o que ele quer dizer com "métodos de extensão não se aplicam". Métodos de extensão são possíveis com qualquer classe. Certamente, a biblioteca do framework .NET é fornecida com muitos métodos de extensão chamados LINQ, que se aplicam às classes genéricas de coleção, mas isso não significa que os métodos de extensão não se aplicam a outras classes? Talvez Ruudjah esteja afirmando que não é óbvio à primeira vista? ;-)
Craig

"métodos de extensão não se aplicam." Onde você lê isso? Estou dizendo que "os métodos de extensão podem ser aplicados.
Ruudjah

11
A implementação do tipo não informa se os métodos de extensão se aplicam ou não, certo? Apenas o fato de que é uma extensão de meio classe métodos que se aplicam. Qualquer consumidor do seu tipo pode criar métodos de extensão completamente independentemente do seu código. Eu acho que é o que estou falando ou perguntando. Você está apenas falando sobre o LINQ? O LINQ é implementado usando métodos de extensão. Mas a ausência de métodos de extensão específicos do LINQ para um tipo específico não significa que os métodos de extensão para esse tipo não existam ou não existam. Eles podem ser criados a qualquer momento por qualquer pessoa.
Craig
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.