Por que structs e classes separam conceitos em C #?

44

Durante a programação em C #, me deparei com uma decisão de design de linguagem estranha que simplesmente não consigo entender.

Portanto, C # (e o CLR) tem dois tipos de dados agregados: struct (tipo de valor, armazenado na pilha, sem herança) e class (tipo de referência, armazenado na pilha, tem herança) .

Essa configuração soa bem no começo, mas você se depara com um método que usa um parâmetro de um tipo agregado e, para descobrir se ele é realmente de um tipo de valor ou de um tipo de referência, você precisa encontrar a declaração do tipo. . Pode ficar realmente confuso às vezes.

A solução geralmente aceita para o problema parece declarar todos os struct s como "imutáveis" (definindo seus campos como readonly ) para evitar possíveis erros, limitando a utilidade de struct s '.

O C ++, por exemplo, emprega um modelo muito mais utilizável: ele permite criar uma instância de objeto na pilha ou no heap e passá-lo por valor ou por referência (ou por ponteiro). Eu continuo ouvindo que o C # foi inspirado pelo C ++, e eu simplesmente não consigo entender por que ele não assumiu essa técnica. Combinando class e struct em uma construção com duas opções de alocação diferentes (heap e pilha) e passando-as como valores ou (explicitamente) como referências através das palavras-chave ref e out parece ser uma coisa boa. / p>

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

    
por Mints97 26.02.2015 / 21:39
fonte

4 respostas

58

O motivo pelo qual o C # (e Java e essencialmente todas as outras linguagens OO desenvolvidas após C ++) não copiaram o modelo de C ++ neste aspecto é porque a forma como o C ++ faz é 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. Os tipos de herança e valor (ou mais especificamente, polimorfismo e valor de passagem) não se misturam; se você passar um objeto do tipo Derived para um argumento de método do tipo Base e, em seguida, chamar um método virtual, a única maneira de obter o comportamento adequado é garantir que o que foi passado seja uma referência.

Entre essa e todas as outras bagunças que você executa em C ++, tendo objetos herdáveis como tipos de valor (construtores de cópia e fatiamento de objetos vem à mente!) a melhor solução é Just Say No.

O bom design da linguagem não é apenas implementar recursos, mas também saber quais recursos não são implementados, e uma das melhores maneiras de fazer isso é aprender com os erros daqueles que vieram antes de você.

    
por 26.02.2015 / 21:49
fonte
19
Por analogia, o C # é basicamente como um conjunto de ferramentas mecânicas onde alguém leu que você geralmente deve evitar alicates e chaves ajustáveis, por isso não inclui chaves ajustáveis, e os alicates são travados em uma gaveta especial marcada. "inseguro" e só pode ser usado com a aprovação de um supervisor, após assinar um aviso de isenção que isenta seu empregador de qualquer responsabilidade por sua saúde.

O C ++, por comparação, não inclui apenas alicates ajustáveis e alicates, mas ferramentas especiais para fins especiais, cuja finalidade não é imediatamente aparente, e se você não sabe o jeito certo de segurá-las, elas podem facilmente corte o polegar (mas, depois de entender como usá-los, 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 mecânico, fresadora, retificadora de superfície, serra de fita para corte de metal, etc., para permitir que você crie e crie ferramentas totalmente novas sempre que sentir necessidade (mas, sim, essas ferramentas podem e ferimentos graves, se você não sabe o que está fazendo com eles - ou até mesmo se você ficar descuidado).

Isso reflete a diferença básica na filosofia: o C ++ tenta fornecer todas as ferramentas que você pode precisar para qualquer projeto que você queira. Não faz quase nenhuma tentativa de 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, assim como projetos que provavelmente são apenas uma péssima ideia e ninguém sabe de uma situação em que Eles provavelmente trabalharão bem. Em particular, muito disso é feito através do desacoplamento de decisões de projeto - mesmo aquelas que na prática são quase sempre acopladas. Como resultado, há uma enorme diferença entre escrever C ++ e escrever C ++. Para escrever C ++ bem, você precisa conhecer muitas expressões idiomáticas e regras práticas (incluindo regras gerais sobre a seriedade de reconsiderar antes de quebrar outras regras básicas). Como resultado, o C ++ é muito mais orientado para a facilidade de uso (por especialistas) do que a facilidade de aprendizado. Há também (todas as demais) circunstâncias em que não é realmente muito fácil de usar também.

O C # faz muito mais para tentar forçar (ou pelo menos extremamente sugerir strongmente) o que os projetistas de linguagem consideravam boas práticas de projeto. Algumas coisas que são desacopladas em C ++ (mas geralmente juntas na prática) são diretamente acopladas em C #. Ele permite que o código "inseguro" empurre os limites um pouco, mas honestamente, não muito.

O resultado é que, por um lado, existem alguns designs que podem ser expressos de forma bastante direta em C ++, que são substancialmente mais desajeitados para serem expressos em C #. Por outro lado, é muito mais fácil aprender C #, e as chances de produzir um design realmente horrível que não funcione para sua situação (ou provavelmente qualquer outra) são drasticamente < Reduzido. Em muitos (provavelmente até a maioria) casos, você pode obter um design sólido e funcional simplesmente "indo com o fluxo", por assim dizer. Ou, como um dos meus amigos (pelo menos eu gosto de pensar nele como um amigo - não tenho certeza se ele realmente concorda) gosta de colocá-lo, o C # torna fácil cair no poço do sucesso.

Então, olhando mais especificamente para a questão de como class e struct tem como estão nas duas linguagens: objetos criados em uma hierarquia de herança onde você pode usar um objeto de uma classe derivada na forma de sua base classe / interface, você está praticamente preso ao fato de que normalmente 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 alguma memória que pode ser tratado como uma instância da classe / interface base, 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 trabalhe com classes polimórficas em uma hierarquia o faça através de um ponteiro ou referência. para a classe base.

Em C #, o que é fundamentalmente a mesma separação entre os tipos é muito mais explícito e aplicado pela própria linguagem. O programador não precisa tomar nenhuma medida para passar uma instância de uma classe por referência, porque isso acontecerá por padrão.

    
por 27.02.2015 / 17:32
fonte
7

Isso é de "C #: Por que precisamos de outro idioma?" - Gunnerson, Eric:

Simplicity was an important design goal for C#.

It's possible to go overboard on simplicity and language purity but purity for purity's sake is of little use to the professional programmer. We therefore tried to balance our desire to have a simple and concise language with solving the real-world problems that programmers face.

[...]

Value types, operator overloading and user-defined conversions all add complexity to the language, but allow an important user scenario to be tremendously simplified.

A semântica de referência para objetos é uma maneira de evitar muitos problemas (é claro e não apenas segmentação de objetos), mas problemas do mundo real podem exigir objetos com valor semântico (por exemplo, dê uma olhada em Parece que eu nunca deveria usar semântica de referência, certo? para um ponto de vista diferente.

Qual a melhor abordagem a tomar, portanto, do que segregar os objetos sujos, feios e ruins com valor semântico sob a tag struct ?

    
por 27.02.2015 / 16:09
fonte
4

Em vez de pensar em tipos de valor derivados de Object , seria mais útil pensar em tipos de locais de armazenamento existentes em um universo totalmente separado dos tipos de instância de classe, mas para cada tipo de valor ter um objeto heap correspondente tipo. Um local de armazenamento do tipo de estrutura simplesmente manté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 = novo boxed_Point (somePoint);     Console.WriteLine ("O valor é {0}", box1);

Na prática, como os tipos de locais de armazenamento e os tipos de instâncias de heap existem em universos separados, não é necessário chamar os tipos de instâncias heap como, por exemplo, boxed_Int32 ; já que o sistema saberia quais contextos exigem a instância do objeto heap e quais requerem um local de armazenamento.

Algumas pessoas acham que qualquer tipo de valor que não se comporte como objetos deve ser considerado "maligno". Admito a visão oposta: como os locais de armazenamento de tipos de valor não são objetos nem referências a objetos, a expectativa de que eles devem se comportar como objetos deve ser considerada inútil. Nos casos em que uma struct pode se comportar de maneira útil como um objeto, não há nada de errado em fazer isso, mas cada struct é nada mais do que uma agregação de campos públicos e privados unidos à fita adesiva.

    
por 27.02.2015 / 19:18
fonte