Enquanto programava em C #, me deparei com uma decisão estranha de design de linguagem que simplesmente não consigo entender.
Portanto, o C # (e o CLR) possui dois tipos de dados agregados: struct
(tipo de valor, armazenado na pilha, sem herança) e class
(tipo de referência, armazenado no heap, possui herança).
Essa configuração parece boa no começo, mas você encontra um método que usa um parâmetro de um tipo agregado e, para descobrir se é realmente de um tipo de valor ou de referência, você precisa encontrar a declaração do tipo. Às vezes, pode ficar realmente confuso.
A solução geralmente aceita para o problema parece declarar todos struct
s como "imutáveis" (configurando seus campos para readonly
) para evitar possíveis erros, limitando struct
a utilidade de s.
O C ++, por exemplo, emprega um modelo muito mais utilizável: permite criar uma instância de objeto na pilha ou na pilha e transmiti-la por valor ou por referência (ou por ponteiro). Eu continuo ouvindo que o C # foi inspirado em C ++ e simplesmente não consigo entender por que ele não adotou essa técnica. Combinar class
e struct
em um construto com duas opções diferentes de alocação (heap e stack) e transmiti-las como valores ou (explicitamente) como referências pelas palavras ref
- out
chave e parecem uma coisa agradável.
A questão é: por que class
e struct
se tornaram conceitos separados em C # e CLR em vez de um tipo agregado com duas opções de alocação?
fonte
struct
nem sempre os s são armazenados na pilha; considere um objeto com umstruct
campo. Além disso, como Mason Wheeler mencionou, o problema da fatia é provavelmente o maior motivo.Respostas:
A razão pela qual C # (e Java e essencialmente todas as outras linguagens OO desenvolvidas após o C ++) não copiaram o modelo do C ++ nesse aspecto é porque o modo como o C ++ faz isso é uma bagunça horrenda.
Você identificou corretamente os pontos relevantes acima::
struct
tipo de valor, sem herança.class
: tipo de referência, tem herança. Tipos de herança e valor (ou mais especificamente, polimorfismo e passagem por valor) não se misturam; se você passar um objeto do tipoDerived
para um argumento de método do tipoBase
e depois chamar um método virtual, a única maneira de obter um comportamento adequado é garantir que o que foi passado seja uma referência.Entre isso e todas as outras bagunças que você encontra no C ++, tendo objetos herdáveis como tipos de valor (construtores de cópia e divisão de objetos vêm à mente!), A melhor solução é Just Say No.
Um bom design de linguagem não é apenas implementar recursos, mas também saber quais recursos não implementar, e uma das melhores maneiras de fazer isso é aprender com os erros daqueles que vieram antes de você.
fonte
Por analogia, o C # é basicamente como um conjunto de ferramentas mecânicas em que alguém leu que você geralmente deve evitar alicates e chaves ajustáveis, para que não inclua chaves ajustáveis, e os alicates estão trancados em uma gaveta especial marcada como "insegura" , e só pode ser usado com a aprovação de um supervisor, depois de assinar um aviso de isenção, exonerando seu empregador de qualquer responsabilidade por sua saúde.
O C ++, por comparação, não inclui apenas chaves e alicates ajustáveis, mas também algumas ferramentas especiais para fins esquisitos cujo objetivo não é imediatamente aparente e, se você não souber a maneira correta de segurá-los, eles podem facilmente cortar seu thumb (mas depois de entender como usá-los, você pode fazer coisas que são essencialmente impossíveis com as ferramentas básicas da caixa de ferramentas C #). Além disso, possui um torno, fresadora, retificadora de superfície, serra de fita para corte de metal, etc., para permitir que você projete e crie ferramentas totalmente novas sempre que sentir necessidade (mas sim, as ferramentas desses maquinistas podem e causarão ferimentos graves se você não souber o que está fazendo com eles - ou mesmo se ficar descuidado).
Isso reflete a diferença básica na filosofia: o C ++ tenta fornecer a você todas as ferramentas necessárias para essencialmente qualquer design que você queira. Ele quase não tenta controlar como você usa essas ferramentas, por isso também é fácil usá-las para produzir designs que só funcionam bem em situações raras, bem como desenhos que provavelmente são apenas uma péssima idéia e ninguém sabe de uma situação em que é provável que funcionem bem. Em particular, muito disso é feito dissociando as decisões de design - mesmo as que na prática são quase sempre acopladas. Como resultado, há uma enorme diferença entre apenas escrever C ++ e escrever bem C ++. Para escrever bem o C ++, você precisa conhecer muitos idiomas e regras práticas (incluindo regras básicas sobre a gravidade da reconsideração antes de violar outras regras básicas). Como um resultado, O C ++ é muito mais orientado à facilidade de uso (por especialistas) do que à facilidade de aprendizado. Existem também (muitas) circunstâncias em que também não é muito fácil de usar.
O C # faz muito mais para tentar forçar (ou pelo menos sugerir extremamente fortemente) o que os designers de linguagem consideraram boas práticas de design. Algumas coisas que são dissociadas em C ++ (mas geralmente andam juntas na prática) são diretamente acopladas em C #. Ele permite que o código "inseguro" ultrapasse os limites um pouco, mas honestamente, não muito.
O resultado é que, por um lado, existem vários designs que podem ser expressos diretamente diretamente em C ++ que são substancialmente mais desajeitados para serem expressos em C #. Por outro lado, é um todo muito mais fácil de aprender C #, e as chances de produzir um design realmente horrível que não vai funcionar para sua situação (ou, provavelmente, qualquer outro) são drasticamente reduzidos. Em muitos casos (provavelmente até na maioria), você pode obter um design sólido e viável simplesmente "seguindo o fluxo", por assim dizer. Ou, como um de meus amigos (pelo menos eu gosto de pensar nele como um amigo - não tenho certeza se ele realmente concorda) gosta de dizer, o C # facilita a queda no poço do sucesso.
Então, olhando mais especificamente para a questão de como
class
estruct
como eles estão nas duas linguagens: objetos criados em uma hierarquia de herança, na qual você pode usar um objeto de uma classe derivada sob o disfarce de sua classe / interface base, preso ao fato de que normalmente você precisa fazer isso por meio de algum tipo de ponteiro ou referência - em um nível concreto, o que acontece é que o objeto da classe derivada contém algo de memória que pode ser tratado como uma instância da classe base / interface, e o objeto derivado é manipulado através do endereço dessa parte da memória.Em C ++, cabe ao programador fazer isso corretamente - quando ele está usando herança, cabe a ele garantir que (por exemplo) uma função que funcione com classes polimórficas em uma hierarquia o faça por meio de um ponteiro ou referência à base classe.
Em C #, o que é fundamentalmente a mesma separação entre os tipos é muito mais explícito e imposto pelo próprio idioma. O programador não precisa executar nenhuma etapa para passar uma instância de uma classe por referência, porque isso acontecerá por padrão.
fonte
Isto é de "C #: por que precisamos de outro idioma?" - Gunnerson, Eric:
A semântica de referência para objetos é uma maneira de evitar muitos problemas (é claro, e não apenas a divisão de objetos), mas os problemas do mundo real às vezes podem exigir objetos com semântica de valor (por exemplo, dê uma olhada em Parece que eu nunca deveria usar semântica de referência, certo? para um ponto de vista diferente).
Que melhor abordagem a ser adotada, portanto, do que segregar os objetos sujos, feios e ruins com valor semântico sob a tag de
struct
?fonte
int[]
deve ser compartilhável ou alterável (as matrizes podem ser uma, mas geralmente não as duas) ajudaria a fazer com que o código errado parecesse errado.Em vez de pensar nos tipos de valor derivados
Object
, seria mais útil pensar nos tipos de local de armazenamento existentes em um universo totalmente separado dos tipos de instância de classe, mas para cada tipo de valor ter um tipo de objeto de pilha correspondente. Um local de armazenamento do tipo de estrutura simplesmente contém uma concatenação dos campos público e privado do tipo, e o tipo de heap é gerado automaticamente de acordo com um padrão como:e para uma declaração como: Console.WriteLine ("O valor é {0}", somePoint);
para ser traduzido como: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("O valor é {0}", caixa1);
Na prática, como os tipos de local de armazenamento e de instância de heap existem em universos separados, não é necessário chamar os tipos de instância de heap como coisas como
boxed_Int32
; pois o sistema saberia quais contextos exigem a instância do objeto de heap e quais exigem um local de armazenamento.Algumas pessoas pensam que quaisquer tipos de valor que não se comportam como objetos devem ser considerados "maus". Sou da opinião oposta: como os locais de armazenamento dos tipos de valor não são objetos nem referências a objetos, a expectativa de que eles se comportem como objetos deve ser considerada inútil. Nos casos em que uma estrutura pode se comportar de maneira útil como um objeto, não há nada de errado em fazê-lo, mas cada um deles não
struct
é nada além de uma agregação de campos públicos e privados presos com fita adesiva.fonte