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?
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?
Respostas:
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 FooType
for 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 FooType
vez 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 Base
e Derived
sã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*=2
seria realmente estar modificando values[0].B
!
Tente depurar ISSO !
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?
Foo
herança de struct Bar
não deve permitir que um Foo
seja atribuído a um Bar
, mas declarar uma estrutura dessa maneira pode permitir alguns efeitos úteis: (1) Crie um membro do tipo especialmente nomeado Bar
como o primeiro item Foo
e Foo
inclua nomes de membros com nomes alternativos para esses membros Bar
, permitindo que o código que antes Bar
era adaptado usasse um Foo
, sem precisar substituir todas as referências thing.BarMember
por thing.theBar.BarMember
e mantendo a capacidade de ler e gravar todos Bar
os campos como um grupo; ...
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.
Foo
que tenha um campo de tipo de estrutura Bar
capaz de considerar Bar
os membros de si mesmos, para que uma Point3d
classe poderia, por exemplo, encapsular um Point2d xy
mas se referir ao X
desse campo como um xy.X
ou X
.
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 struct
que 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.
Point3D
partir de Point2D
; você não seria capaz para usar um Point3D
em vez de um Point2D
, mas você não teria que reimplementar o Point3D
inteiramente a partir do zero) É assim que eu interpretada de qualquer maneira ....
class
sobre struct
quando apropriado.
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.
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. :-)
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.
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á.
As estruturas suportam interfaces, para que você possa fazer algumas coisas polimórficas dessa maneira.
IL é uma linguagem baseada em pilha, portanto, chamar um método com um argumento é algo como isto:
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.