Por que as estruturas não suportam herança?

128

Eu sei que estruturas no .NET não suportam herança, mas não está exatamente claro por que elas são limitadas dessa maneira.

Que razão técnica impede que as estruturas sejam herdadas de outras estruturas?

Julieta
fonte
6
Não estou morrendo de vontade dessa funcionalidade, mas posso pensar em alguns casos em que a herança de estrutura seria útil: convém estender uma estrutura Point2D a uma estrutura Point3D com herança, você pode herdar do Int32 para restringir seus valores entre 1 e 100, você pode querer criar um tipo de definição que é visível em vários arquivos (o Usando typeA = truque typeB tem apenas escopo do arquivo), etc.
Julieta
4
Você pode ler stackoverflow.com/questions/1082311/… , que explica um pouco mais sobre estruturas e por que elas devem ser restritas a um determinado tamanho. Se você deseja usar herança em uma estrutura, provavelmente deve estar usando uma classe.
266 Justin Justin
1
E você pode querer ler stackoverflow.com/questions/1222935/…, uma vez que detalha por que simplesmente não pôde ser feito na plataforma dotNet. Eles fizeram da maneira C ++, com os mesmos problemas que podem ser desastrosos para uma plataforma gerenciada.
Dykam 03/08/09
As classes @Justin têm custos de desempenho que as estruturas podem evitar. E no desenvolvimento de jogos que realmente importa. Portanto, em alguns casos, você não deve usar uma classe se puder ajudá-la.
Gavin Williams
@ Tykam Acho que isso pode ser feito em c #. Desastroso é um exagero. Hoje, posso escrever códigos desastrosos em C # quando não estou familiarizado com uma técnica. Portanto, isso não é realmente um problema. Se a herança de estruturas puder resolver alguns problemas e fornecer um melhor desempenho em determinados cenários, então eu sou a favor.
Gavin Williams

Respostas:

121

A razão pela qual os tipos de valor não podem suportar herança é por causa de matrizes.

O problema é que, por razões de desempenho e GC, matrizes de tipos de valor são armazenadas "em linha". Por exemplo, dado que new FooType[10] {...}, se FooTypefor um tipo de referência, 11 objetos serão criados no heap gerenciado (um para a matriz e 10 para cada instância de tipo). Se, em FooTypevez disso, for um tipo de valor, apenas uma instância será criada no heap gerenciado - para a própria matriz (pois cada valor da matriz será armazenado "em linha" com a matriz).

Agora, suponha que tivéssemos herança com tipos de valor. Quando combinado com o comportamento acima de "armazenamento embutido" de matrizes, Bad Things acontece, como pode ser visto em C ++ .

Considere este código pseudo-C #:

struct Base
{
    public int A;
}

struct Derived : Base
{
    public int B;
}

void Square(Base[] values)
{
  for (int i = 0; i < values.Length; ++i)
      values [i].A *= 2;
}

Derived[] v = new Derived[2];
Square (v);

Pelas regras normais de conversão, a Derived[]é conversível em a Base[](para melhor ou para pior), portanto, se você s / struct / class / g no exemplo acima, ele será compilado e executado conforme o esperado, sem problemas. Mas se Basee Derivedsão tipos de valor e matrizes armazenam valores em linha, então temos um problema.

Temos um problema porque Square()não sabemos nada Derived, ele usará apenas a aritmética do ponteiro para acessar cada elemento da matriz, incrementando em uma quantidade constante ( sizeof(A)). A assembléia seria vagamente como:

for (int i = 0; i < values.Length; ++i)
{
    A* value = (A*) (((char*) values) + i * sizeof(A));
    value->A *= 2;
}

(Sim, é uma montagem abominável, mas o ponto é que incrementaremos a matriz em constantes conhecidas em tempo de compilação, sem o conhecimento de que um tipo derivado está sendo usado.)

Portanto, se isso realmente acontecesse, teríamos problemas de corrupção de memória. Especificamente, dentro Square(), values[1].A*=2seria realmente estar modificando values[0].B!

Tente depurar ISSO !

jonp
fonte
11
A solução sensata para esse problema seria proibir a forma de elenco Base [] para Detived []. Assim como a conversão de short [] para int [] é proibida, embora a conversão de short para int seja possível.
Niki
3
+ resposta: o problema com a herança não clicou comigo até que você o colocasse em termos de matrizes. Outro usuário afirmou que esse problema pode ser atenuado por "cortar" estruturas no tamanho apropriado, mas considero que a fatia é a causa de mais problemas do que resolve.
Julieta
4
Sim, mas isso "faz sentido" porque as conversões de matriz são para implícitas, não para explícitas. short to int é possível, mas requer uma conversão, portanto, é sensato que short [] não possa ser convertido em int [] (menos do código de conversão, como 'a.Select (x => (int) x) .ToArray ( ) '). Se o tempo de execução não permitisse a transmissão da Base para Derivada, seria uma "verruga", pois é permitido para tipos de referência. Portanto, temos duas "verrugas" diferentes possíveis - proibir a herança de estruturas ou proibir conversões de matrizes derivadas em matrizes de base.
jonp
3
Pelo menos impedindo a herança da estrutura, temos uma palavra-chave separada e podemos dizer com mais facilidade "estruturas são especiais", em vez de ter uma limitação "aleatória" em algo que funciona para um conjunto de coisas (classes), mas não para outro (estruturas) . Imagino que a limitação da estrutura seja muito mais fácil de explicar ("são diferentes!").
jonp
2
necessidade de mudar o nome da função de 'quadrado' para 'duplo'
João
69

Imagine estruturas suportadas herança. Então declarando:

BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.

a = b; //?? expand size during assignment?

significaria que variáveis ​​de estrutura não têm tamanho fixo, e é por isso que temos tipos de referência.

Melhor ainda, considere o seguinte:

BaseStruct[] baseArray = new BaseStruct[1000];

baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Kenan EK
fonte
5
O C ++ respondeu a isso introduzindo o conceito de 'fatia', então esse é um problema solucionável. Então, por que a herança de estruturas não deve ser suportada?
jonp
1
Considere matrizes de estruturas herdáveis ​​e lembre-se de que o C # é uma linguagem gerenciada (memória). Fatiar ou qualquer outra opção semelhante causaria estragos nos fundamentos do CLR.
Kenan EK
4
@ Jonp: Solvable, sim. Desejável? Aqui está um experimento mental: imagine se você tem uma classe base Vector2D (x, y) e uma classe derivada Vector3D (x, y, z). Ambas as classes têm uma propriedade Magnitude que calcula sqrt (x ^ 2 + y ^ 2) e sqrt (x ^ 2 + y ^ 2 + z ^ 2) respectivamente. Se você escrever 'Vector3D a = Vector3D (5, 10, 15); Vetor2D b = a; ', o que' a.Magnitude == b.Magnitude 'deve retornar? Se escrevermos 'a = (Vector3D) b', a.Magnitude terá o mesmo valor antes da atribuição que após? Os designers do .NET provavelmente disseram a si mesmos: "não, não teremos nada disso".
Juliet
1
Só porque um problema pode ser resolvido, não significa que ele deva ser resolvido. Às vezes, é melhor evitar situações em que o problema surge.
21700 Dan Diplo
@ kek444: Ter uma Fooherança de struct Barnão deve permitir que um Fooseja atribuído a um Bar, mas declarar uma estrutura dessa maneira pode permitir alguns efeitos úteis: (1) Crie um membro do tipo especialmente nomeado Barcomo o primeiro item Fooe Fooinclua nomes de membros com nomes alternativos para esses membros Bar, permitindo que o código que antes Barera adaptado usasse um Foo, sem precisar substituir todas as referências thing.BarMemberpor thing.theBar.BarMembere mantendo a capacidade de ler e gravar todos Baros campos como um grupo; ...
supercat
14

Estruturas não usam referências (a menos que estejam na caixa, mas você deve tentar evitar isso), portanto, o polimorfismo não é significativo, pois não há indireto por meio de um ponteiro de referência. Os objetos normalmente vivem no heap e são referenciados por ponteiros de referência, mas as estruturas são alocadas na pilha (a menos que estejam encaixotadas) ou alocadas "dentro" da memória ocupada por um tipo de referência no heap.

Martin Liversage
fonte
8
um não precisa polimorfismo uso para tirar proveito da herança
rmeador
Então, você teria quantos tipos diferentes de herança no .NET?
John Saunders
O polimorfismo existe em estruturas, apenas considere a diferença entre chamar ToString () quando você implementá-lo em uma estrutura personalizada ou quando uma implementação personalizada de ToString () não existe.
Kenan EK
Isso porque todos eles derivam do System.Object. É mais o polimorfismo do tipo System.Object do que de estruturas.
John Saunders
O polimorfismo pode ser significativo com estruturas usadas como parâmetros de tipo genérico. O polimorfismo trabalha com estruturas que implementam interfaces; o maior problema com as interfaces é que elas não podem expor byrefs para estruturar campos. Caso contrário, a maior coisa que eu acho que seria útil na medida em que "herdar" estruturas seria um meio de ter um tipo (struct ou classe) Fooque tenha um campo de tipo de estrutura Barcapaz de considerar Baros membros de si mesmos, para que uma Point3dclasse poderia, por exemplo, encapsular um Point2d xymas se referir ao Xdesse campo como um xy.Xou X.
Supercat 01/10
8

Aqui está o que os documentos dizem:

Estruturas são particularmente úteis para pequenas estruturas de dados que possuem semântica de valor. Números complexos, pontos em um sistema de coordenadas ou pares de valores-chave em um dicionário são bons exemplos de estruturas. A chave para essas estruturas de dados é que eles têm poucos membros de dados, que não exigem o uso de herança ou identidade referencial e que eles podem ser implementados convenientemente usando a semântica de valores em que a atribuição copia o valor em vez da referência.

Basicamente, eles devem conter dados simples e, portanto, não possuem "recursos extras", como herança. Provavelmente seria tecnicamente possível para eles oferecerem suporte a algum tipo limitado de herança (não polimorfismo, por estarem na pilha), mas acredito que também é uma opção de design para não dar suporte à herança (como muitas outras coisas no .NET idiomas são.)

Por outro lado, concordo com os benefícios da herança e acho que todos atingimos o ponto em que queremos structque herdemos do outro e compreendemos que isso não é possível. Mas, nesse ponto, a estrutura de dados provavelmente é tão avançada que deveria ser uma classe de qualquer maneira.

Blixt
fonte
4
Essa não é a razão pela qual não há herança.
Dykam 03/08/09
Acredito que a herança mencionada aqui não seja capaz de usar duas estruturas em que uma seja herdada da outra de forma intercambiável, mas reutilizando e adicionando à implementação de uma estrutura na outra (ou seja, criando uma a Point3Dpartir de Point2D; você não seria capaz para usar um Point3Dem vez de um Point2D, mas você não teria que reimplementar o Point3Dinteiramente a partir do zero) É assim que eu interpretada de qualquer maneira ....
Blixt
Resumindo: poderia apoiar a herança sem polimorfismo. Não faz. Eu acredito que é uma escolha de design para ajudar uma pessoa escolha classsobre structquando apropriado.
Blixt
3

Classe como herança não é possível, pois uma estrutura é colocada diretamente na pilha. Uma estrutura herdada seria maior que a mãe, mas o JIT não sabe disso e tenta colocar muito menos espaço. Parece um pouco claro, vamos escrever um exemplo:

struct A {
    int property;
} // sizeof A == sizeof int

struct B : A {
    int childproperty;
} // sizeof B == sizeof int * 2

Se isso fosse possível, haveria um erro fatal no seguinte trecho:

void DoSomething(A arg){};

...

B b;
DoSomething(b);

O espaço é alocado para o tamanho de A, não para o tamanho de B.

Dykam
fonte
2
C ++ lida com esse caso muito bem, IIRC. A instância de B é cortada para caber no tamanho de um A. Se for um tipo de dados puro, como são as estruturas do .NET, nada de ruim acontecerá. Você se depara com um problema com um método que retorna um A e está armazenando esse valor de retorno em um B, mas isso não deve ser permitido. Em resumo, os designers do .NET poderiam ter lidado com isso se quisessem, mas não o fizeram por algum motivo.
Rmeador
1
Para o seu DoSomething (), não é provável que exista um problema, pois (assumindo a semântica do C ++) 'b' seria "fatiado" para criar uma instância A. O problema está nas <i> matrizes </i>. Considere suas estruturas A e B existentes e um método <c> DoSomething (A [] arg) {arg [1] .property = 1;} </c>. Como matrizes de tipos de valor armazenam os valores "em linha", DoSomething (atual = novo B [2] {}) fará com que a propriedade [0] .child real seja definida, não a propriedade [1]. Isto é mau.
jonp
2
@ John: Eu não estava afirmando que era, e eu também não acho que @ Jonp. Estávamos apenas mencionando que esse problema é antigo e foi resolvido; portanto, os designers do .NET optaram por não dar suporte a ele por algum motivo que não fosse inviável técnica.
Rmeador
Note-se que o problema "matrizes de tipos derivados" não é novo no C ++; veja parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4 (Arrays em C ++ são maus ;-)!
jonp
@ John: a solução para "matrizes de tipos derivados e tipos de base não se misturam" é, como sempre, Não faça isso. É por isso que as matrizes em C ++ são más (permite mais facilmente a corrupção de memória) e o .NET não suporta herança com tipos de valor (o compilador e o JIT garantem que isso não ocorra).
jonp
3

Há um ponto que eu gostaria de corrigir. Embora a razão pela qual as estruturas não possam ser herdadas seja porque elas vivem na pilha é a correta, é ao mesmo tempo uma explicação parcialmente correta. Estruturas, como qualquer outro tipo de valor, podem viver na pilha. Como isso dependerá de onde a variável é declarada, eles viverão na pilha ou na pilha . Será quando eles forem variáveis ​​locais ou campos de instância, respectivamente.

Ao dizer isso, Cecil Has a Name acertou em cheio.

Eu gostaria de enfatizar isso, os tipos de valor podem viver na pilha. Isso não significa que eles sempre fazem isso. Variáveis ​​locais, incluindo parâmetros de método, serão. Todos os outros não. Ainda assim, continua sendo a razão pela qual eles não podem ser herdados. :-)

Rui Craveiro
fonte
3

As estruturas são alocadas na pilha. Isso significa que a semântica de valores é praticamente gratuita e o acesso a membros de estrutura é muito barato. Isso não impede o polimorfismo.

Você pode iniciar cada estrutura com um ponteiro para sua tabela de funções virtuais. Isso seria um problema de desempenho (cada estrutura teria pelo menos o tamanho de um ponteiro), mas é factível. Isso permitiria funções virtuais.

Que tal adicionar campos?

Bem, quando você aloca uma estrutura na pilha, aloca uma certa quantidade de espaço. O espaço necessário é determinado no tempo de compilação (antecipadamente ou ao JITting). Se você adicionar campos e depois atribuir a um tipo de base:

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

Isso substituirá uma parte desconhecida da pilha.

A alternativa é que o tempo de execução impeça isso gravando apenas sizeof (A) bytes em qualquer variável A.

O que acontece se B substituir um método em A e referenciar seu campo Integer2? O tempo de execução lança uma MemberAccessException ou o método acessa alguns dados aleatórios na pilha. Nenhuma delas é permitida.

É perfeitamente seguro ter herança de estrutura, desde que você não use estruturas polimorficamente ou contanto que você não adicione campos ao herdar. Mas estes não são terrivelmente úteis.

user38001
fonte
1
Quase. Ninguém mais mencionou o problema de fatia em referência à pilha, apenas em referência a matrizes. E mais ninguém mencionou as soluções disponíveis.
User38001 03/08/2009
1
Todos os tipos de valor em .net são preenchidos com zero sem vincos, independentemente de seu tipo ou de quais campos eles contêm. Adicionar algo como um ponteiro vtable a uma estrutura exigiria um meio de inicializar tipos com valores padrão diferentes de zero. Esse recurso pode ser útil para uma variedade de propósitos, e a implementação de um recurso para a maioria dos casos pode não ser muito difícil, mas nada existe no .net.
Supercat
@ user38001 "As estruturas estão alocadas na pilha" - a menos que sejam campos de instância, caso em que estão alocadas na pilha.
David Klempfner
2

Esta parece ser uma pergunta muito frequente. Eu sinto como adicionar que os tipos de valor são armazenados "no local" onde você declara a variável; além dos detalhes da implementação, isso significa que não há um cabeçalho de objeto que diga algo sobre o objeto, apenas a variável sabe que tipo de dados reside lá.

Cecil tem um nome
fonte
O compilador sabe o que está lá. Referenciar C ++ não pode ser a resposta.
Henk Holterman
De onde você inferiu o C ++? Eu diria no local porque é o que melhor combina com o comportamento, a pilha é um detalhe de implementação, para citar um artigo de blog do MSDN.
Cecil tem um nome
Sim, mencionar C ++ era ruim, apenas minha linha de pensamento. Mas, além da pergunta, se as informações de tempo de execução são necessárias, por que as estruturas não devem ter um 'cabeçalho de objeto'? O compilador pode misturá-los da maneira que desejar. Pode até ocultar um cabeçalho em uma estrutura [Structlayout].
Henk Holterman
Como estruturas são tipos de valor, não é necessário com um cabeçalho de objeto, porque o tempo de execução sempre copia o conteúdo como para outros tipos de valor (uma restrição). Não faria sentido com um cabeçalho, porque é isso que as classes de tipo de referência são para: P
Cecil tem um nome
1

As estruturas suportam interfaces, para que você possa fazer algumas coisas polimórficas dessa maneira.

Wyatt Barnett
fonte
0

IL é uma linguagem baseada em pilha, portanto, chamar um método com um argumento é algo como isto:

  1. Empurre o argumento para a pilha
  2. Chame o método

Quando o método é executado, ele libera alguns bytes da pilha para obter seu argumento. Ele sabe exatamente quantos bytes serão disparados porque o argumento é um ponteiro de tipo de referência (sempre 4 bytes em 32 bits) ou é um tipo de valor para o qual o tamanho sempre é conhecido exatamente.

Se for um ponteiro de tipo de referência, o método pesquisará o objeto no heap e obterá seu identificador de tipo, que aponta para uma tabela de métodos que manipula esse método específico para esse tipo exato. Se for um tipo de valor, nenhuma pesquisa será necessária em uma tabela de métodos, porque os tipos de valor não suportam herança, portanto, existe apenas uma combinação possível de método / tipo.

Se os tipos de valor suportassem herança, haveria uma sobrecarga extra, pois o tipo específico da estrutura teria que ser colocado na pilha, bem como seu valor, o que significaria algum tipo de pesquisa de tabela de método para a instância concreta específica do tipo. Isso eliminaria as vantagens de velocidade e eficiência dos tipos de valor.

Matt Howells
fonte
1
C ++ tem resolvido isso, leia esta resposta para o problema real: stackoverflow.com/questions/1222935/...
dykam