Por que estruturas e classes separam conceitos em C #?

44

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 structs como "imutáveis" (configurando seus campos para readonly) para evitar possíveis erros, limitando structa 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 classe structem 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- outchave e parecem uma coisa agradável.

A questão é: por que classe structse tornaram conceitos separados em C # e CLR em vez de um tipo agregado com duas opções de alocação?

Mints97
fonte
34
"A solução geralmente aceita para o problema parece declarar todas as estruturas como" imutáveis ​​"... limitando a utilidade das estruturas" Muitas pessoas argumentam que tornar algo imutável geralmente o torna mais útil sempre que não é a causa de um gargalo de desempenho. Além disso, structnem sempre os s são armazenados na pilha; considere um objeto com um structcampo. Além disso, como Mason Wheeler mencionou, o problema da fatia é provavelmente o maior motivo.
D
7
Não é verdade que o C # foi inspirado em C ++; em vez disso, o C # foi inspirado por todos os erros (bem-intencionados e com boa aparência na época) no design de C ++ e Java.
Pieter Geerkens
18
Nota: stack e heap são detalhes de implementação. Não há nada que diga que as instâncias struct devem ser alocadas na pilha e as instâncias de classe devem ser alocadas no heap. E de fato nem é verdade. Por exemplo, é muito bem possível que um compilador possa determinar, usando a Análise de Escape, que uma variável não pode escapar do escopo local e alocá-la na pilha, mesmo que seja uma instância de classe. Nem sequer diz que deve haver uma pilha ou uma pilha. Você pode alocar quadros de ambiente como uma lista vinculada no heap e nem sequer ter uma pilha.
Jörg W Mittag 27/02
3
"mas você se depara com 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" Umm, por que isso importa exatamente ? O método pede para você passar um valor. Você passa um valor. Em que momento você precisa se preocupar se é um tipo de referência ou um valor?
Luaan

Respostas:

58

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:: structtipo 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 tipo Derivedpara um argumento de método do tipo Basee 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ê.

Mason Wheeler
fonte
29
Este é apenas mais um discurso sem sentido no C ++. Não posso votar, mas faria se pudesse.
Bartek Banachewicz
17
@MasonWheeler: " é uma bagunça horrenda " parece bastante subjetivo. Isso já foi discutido em um longo tópico de comentários para outra resposta sua ; o tópico ficou nu (infelizmente, porque continha comentários úteis, embora com molho de guerra de chamas). Não acho que valha a pena repetir tudo aqui, mas "C # acertou e C ++ entendeu errado" (que parece ser a mensagem que você está tentando transmitir) é realmente uma afirmação subjetiva.
Andy Prowl
15
@MasonWheeler: Eu fiz no tópico que foi bloqueado, e várias outras pessoas - e é por isso que acho lamentável que ele tenha sido excluído. Eu não acho que seja uma boa ideia replicar esse segmento aqui, mas a versão curta é: em C ++, o usuário do tipo, não seu designer , decide com qual semântica um tipo deve ser usado (semântica ou valor de referência semântica). Isso tem vantagens e desvantagens: você se enfurece contra os contras sem considerar (ou conhecer?) Os profissionais. É por isso que a análise é subjetiva.
Andy Prowl
7
suspiro Eu acredito que a discussão sobre violação do LSP já ocorreu. E eu meio que acredito que a maioria das pessoas concordou que a menção ao LSP é bastante estranha e não relacionada, mas não pode verificar porque um mod anulou o tópico de comentários .
Bartek Banachewicz
9
Se você mover seu último parágrafo para o topo e excluir o primeiro parágrafo atual, acho que você tem o argumento perfeito. Mas o primeiro parágrafo atual é apenas subjetivo.
Martin York
19

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 classe structcomo 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.

Jerry Coffin
fonte
2
Como um fã de C ++, acho que este é um excelente resumo das diferenças entre C # e a serra elétrica do exército suíço.
David Thornley
1
@ DavidThornley: Eu pelo menos tentei escrever o que pensei que seria uma comparação um tanto equilibrada. Não para apontar o dedo, mas parte do que eu vi quando escrevi isso me pareceu ... um pouco imprecisa (para dizer bem).
Jerry Coffin
7

Isto é de "C #: por que precisamos de outro idioma?" - Gunnerson, Eric:

A simplicidade era uma importante meta de design para o C #.

É possível exagerar na simplicidade e pureza da linguagem, mas a pureza por causa da pureza é de pouca utilidade para o programador profissional. Portanto, tentamos equilibrar nosso desejo de ter uma linguagem simples e concisa com a solução dos problemas do mundo real que os programadores enfrentam.

[...]

Tipos de valor , sobrecarga do operador e conversões definidas pelo usuário aumentam a complexidade do idioma , mas permitem que um cenário importante do usuário seja tremendamente simplificado.

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?

manlio
fonte
1
Eu não sei, talvez não usando esses objetos sujos, feios e ruins com semântica de referência?
Bartek Banachewicz
Talvez ... eu seja uma causa perdida.
manlio 27/02/2015
2
IMHO, um dos maiores defeitos no design de Java é a falta de meios para declarar se uma variável está sendo usada para encapsular identidade ou propriedade , e um dos maiores defeitos em C # é a falta de meios para distinguir operações em uma variável de operações em um objeto ao qual uma variável mantém uma referência. Mesmo que o Runtime não se preocupasse com essas distinções, ser capaz de especificar em um idioma se uma variável do tipo 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.
supercat 27/02
4

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:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

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.

supercat
fonte