Por que não consigo definir um construtor padrão para uma estrutura no .NET?

261

No .NET, um tipo de valor (C # struct) não pode ter um construtor sem parâmetros. De acordo com este post, isso é obrigatório pela especificação da CLI. O que acontece é que, para cada tipo de valor, é criado um construtor padrão (pelo compilador?) Que inicializa todos os membros como zero (ou null).

Por que não é permitido definir um construtor padrão?

Um uso trivial é para números racionais:

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

Usando a versão atual do C #, um Rational padrão é o 0/0que não é tão legal.

PS : Os parâmetros padrão ajudarão a resolver isso no C # 4.0 ou o construtor padrão definido pelo CLR será chamado?


Jon Skeet respondeu:

Para usar seu exemplo, o que você gostaria que acontecesse quando alguém fizesse:

 Rational[] fractions = new Rational[1000];

Ele deve executar seu construtor 1000 vezes?

Claro que deveria, foi por isso que escrevi o construtor padrão em primeiro lugar. O CLR deve usar o construtor de zeragem padrão quando nenhum construtor padrão explícito é definido; Dessa forma, você paga apenas pelo que usa. Então, se eu quiser um contêiner de 1000 Rationals não padrão (e quiser otimizar as 1000 construções), usarei List<Rational>uma matriz em vez de uma matriz.

Este motivo, em minha opinião, não é forte o suficiente para impedir a definição de um construtor padrão.

Motti
fonte
3
O +1 teve um problema semelhante uma vez e finalmente converteu a estrutura em uma classe.
Dirk Vollmar
4
Os parâmetros padrão no C # 4 não podem ajudar porque Rational()chama o ctor sem parâmetros em vez de Rational(long num=0, long denom=1).
LaTeX
6
Observe que no C # 6.0, fornecido com o Visual Studio 2015, será permitido gravar construtores de instância de parâmetro zero para estruturas. Então, new Rational()chamará o construtor se ele existir, no entanto, se não existir, new Rational()será equivalente a default(Rational). De qualquer forma, é recomendável usar a sintaxe default(Rational)quando desejar o "valor zero" da sua estrutura (que é um número "ruim" com o design proposto Rational). O valor padrão para um tipo de valor Té sempre default(T). Portanto new Rational[1000], nunca invocará struct constructors.
Jeppe Stig Nielsen
6
Para resolver este problema específico que você pode armazenar denominator - 1no interior da estrutura, de modo que o valor padrão se torna 0/1
miniBill
3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.Por que você esperaria que uma matriz chamasse um construtor diferente para uma lista para uma estrutura?
mjwills

Respostas:

197

Nota: a resposta abaixo foi escrita muito tempo antes do C # 6, que planeja introduzir a capacidade de declarar construtores sem parâmetros em estruturas - mas eles ainda não serão chamados em todas as situações (por exemplo, para criação de matriz) (no final esse recurso não foi adicionado ao C # 6 ).


Edição: Eu editei a resposta abaixo devido à visão de Grauenwolf sobre o CLR.

O CLR permite que os tipos de valor tenham construtores sem parâmetros, mas o C # não. Eu acredito que isso ocorre porque introduziria uma expectativa de que o construtor seria chamado quando não o faria. Por exemplo, considere isso:

MyStruct[] foo = new MyStruct[1000];

O CLR é capaz de fazer isso com muita eficiência apenas alocando a memória apropriada e zerando tudo. Se tivesse que executar o construtor MyStruct 1000 vezes, isso seria muito menos eficiente. (Na verdade, isso não acontece - se você faz tem um construtor sem parâmetros, ele não é executado quando você criar uma matriz, ou quando você tem uma variável de instância não inicializada.)

A regra básica em C # é "o valor padrão para qualquer tipo não pode confiar em nenhuma inicialização". Agora eles poderiam ter permitido que construtores sem parâmetros fossem definidos, mas não exigiam que o construtor fosse executado em todos os casos - mas isso levaria a mais confusão. (Ou pelo menos, então acredito que o argumento continua.)

EDIT: Para usar seu exemplo, o que você gostaria que acontecesse quando alguém fizesse:

Rational[] fractions = new Rational[1000];

Ele deve executar seu construtor 1000 vezes?

  • Caso contrário, acabamos com 1000 argumentos inválidos
  • Se isso acontecer, potencialmente desperdiçamos uma carga de trabalho se estamos prestes a preencher a matriz com valores reais.

EDIT: (Respondendo um pouco mais à pergunta) O construtor sem parâmetros não é criado pelo compilador. Tipos de valor não tem que ter construtores, tanto quanto o CLR está em causa - embora ele sair ele pode se você escrevê-lo em IL. Quando você escreve " new Guid()" em C # que emite IL diferente do que você obtém se chamar um construtor normal. Veja esta pergunta SO para um pouco mais sobre esse aspecto.

Eu suspeito que não há nenhum tipo de valor na estrutura com construtores sem parâmetros. Sem dúvida, o NDepend poderia me dizer se eu pedisse o suficiente ... O fato de o C # proibir é uma dica grande o suficiente para eu pensar que é provavelmente uma má idéia.

Jon Skeet
fonte
9
Explicação mais curta: em C ++, struct e classe eram apenas dois lados da mesma moeda. A única diferença real é que uma era pública por padrão e a outra era privada. No .Net, há uma diferença muito maior entre uma estrutura e uma classe, e é importante entendê-la.
Joel Coehoorn
39
@ Joel: Isso realmente não explica essa restrição específica, explica?
Jon Skeet
6
O CLR permite que os tipos de valor tenham construtores sem parâmetros. E sim, ele será executado para cada elemento em uma matriz. O C # acha que é uma má idéia e não permite, mas você pode escrever uma linguagem .NET que o faça.
Jonathan Allen
2
Desculpe, estou um pouco confuso com o seguinte. Será Rational[] fractions = new Rational[1000];também desperdiçar uma carga de trabalho, se Rationalé uma classe em vez de um struct? Se sim, por que as classes têm um controlador padrão?
beije minha axila
5
@ FifaEarthCup2014: Você teria que ser mais específico sobre o que você quer dizer com "desperdiçar uma carga de trabalho". Mas quando, de qualquer maneira, não chamará o construtor 1000 vezes. Se Rationalfor uma classe, você terá uma matriz de 1000 referências nulas.
Jon Skeet
48

Uma estrutura é um tipo de valor e um tipo de valor deve ter um valor padrão assim que for declarado.

MyClass m;
MyStruct m2;

Se você declarar dois campos como acima, sem instanciar, interrompa o depurador, mserá nulo, mas m2não será. Dado isso, um construtor sem parâmetros não faria sentido; na verdade, todo construtor de uma estrutura faz atribuir valores, a coisa em si já existe apenas ao declará-la. De fato, o m2 poderia muito bem ser usado no exemplo acima e ter seus métodos chamados, se houver, e seus campos e propriedades manipulados!

user42467
fonte
3
Não sei por que alguém votou contra você. Você parece ser a resposta mais correta aqui.
pipTheGeek 3/08/08
12
O comportamento em C ++ é que, se um tipo tiver um construtor padrão, ele será usado quando esse objeto for criado sem um construtor explícito. Isso poderia ter sido usado em C # para inicializar o m2 com o construtor padrão, e é por isso que esta resposta não é útil.
Motti
3
onester: se você não quiser que as estruturas chamem seu próprio construtor quando declaradas, não defina um construtor padrão! :) é o que o Motti está dizendo
Stefan Monov
8
@Tarik. Eu não concordo. Pelo contrário, um construtor sem parâmetros faria todo o sentido: se eu quiser criar uma estrutura "Matrix" que sempre tenha uma matriz de identidade como valor padrão, como você poderia fazê-lo por outros meios?
Elo
1
Não tenho certeza se concordo plenamente com o "Indeed m2 poderia ser usado com muita satisfação .." . Pode ter sido verdade em um C # anterior, mas é um erro do compilador declarar uma estrutura, não newIt, e depois tentar usar seus membros
Caius Jard
18

Embora o CLR permita, o C # não permite que as estruturas tenham um construtor padrão sem parâmetros. O motivo é que, para um tipo de valor, os compiladores, por padrão, não geram um construtor padrão, nem geram uma chamada para o construtor padrão. Portanto, mesmo que você tenha definido um construtor padrão, ele não será chamado e isso apenas o confundirá.

Para evitar esses problemas, o compilador C # não permite a definição de um construtor padrão pelo usuário. E como ele não gera um construtor padrão, você não pode inicializar campos ao defini-los.

Ou o grande motivo é que uma estrutura é um tipo de valor e os tipos de valor são inicializados por um valor padrão e o construtor é usado para a inicialização.

Você não precisa instanciar sua estrutura com a newpalavra - chave. Em vez disso, funciona como um int; você pode acessá-lo diretamente.

As estruturas não podem conter construtores explícitos sem parâmetros. Os membros do Struct são inicializados automaticamente com seus valores padrão.

Um construtor padrão (sem parâmetros) para uma estrutura pode definir valores diferentes do estado com zero, o que seria um comportamento inesperado. Portanto, o tempo de execução do .NET proíbe construtores padrão para uma estrutura.

Adiii
fonte
Esta resposta é de longe a melhor. No final, o objetivo principal da restrição é evitar surpresas, como MyStruct s;não chamar o construtor padrão que você forneceu.
talles 5/11/2015
1
Obrigado pela explicação. Portanto, é apenas uma falta do compilador que precisaria ser aprimorada; não há uma boa razão teórica para proibir construtores sem parâmetros (assim que eles puderem ser restritos a apenas acessar propriedades).
Elo
16

Você pode criar uma propriedade estática que inicialize e retorne um número "racional" padrão:

public static Rational One => new Rational(0, 1); 

E use-o como:

var rat = Rational.One;
AustinWBryan
fonte
24
Nesse caso, Rational.Zeropode ser um pouco menos confuso.
Kevin
13

Explicação mais curta:

Em C ++, struct e classe eram apenas dois lados da mesma moeda. A única diferença real é que um era público por padrão e o outro era privado.

No .NET , há uma diferença muito maior entre uma estrutura e uma classe. O principal é que struct fornece semântica de tipo de valor, enquanto classe fornece semântica de tipo de referência. Quando você começa a pensar nas implicações dessa mudança, outras também começam a fazer mais sentido, incluindo o comportamento do construtor que você descreve.

Joel Coehoorn
fonte
7
Você vai ter que ser um pouco mais explícito sobre como isso está implícito no tipo de referência de valor vs. dividir Eu não entendo ...
Motti
Os tipos de valor têm um valor padrão - eles não são nulos, mesmo que você não defina um construtor. Embora, à primeira vista, isso não exclua também a definição de um construtor padrão, a estrutura que usa esse recurso é interna para fazer certas suposições sobre estruturas.
Joel Coehoorn
@annakata: Outros construtores provavelmente são úteis em alguns cenários que envolvem Reflexão. Além disso, se os genéricos fossem aprimorados para permitir uma restrição "nova" parametrizada, seria útil ter estruturas que pudessem estar em conformidade com elas.
Supercat 24/02/12
@annakata Eu acredito que é porque o C # tem um requisito forte que newrealmente deve ser escrito para chamar um construtor. Em C ++, os construtores são chamados de formas ocultas, na declaração ou instanciação de matrizes. No C #, tudo é um ponteiro; portanto, comece com null, é uma estrutura e deve começar com algo, mas quando você não pode escrever new... (como array init), isso quebraria uma regra forte de C #.
precisa saber é o seguinte
3

Eu não vi o equivalente à solução tardia que vou dar, então aqui está.

use deslocamentos para mover valores do padrão 0 para qualquer valor que desejar. aqui as propriedades devem ser usadas em vez de acessar diretamente os campos. (talvez com o possível recurso do c # 7, você defina melhor os campos com escopo de propriedade para que eles permaneçam protegidos contra o acesso direto no código.)

Esta solução funciona para estruturas simples com apenas tipos de valor (sem tipo de referência ou estrutura anulável).

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

Isso é diferente de resposta, essa abordagem não é uma caixa especial, mas sua utilização de deslocamento que funcionará para todos os intervalos.

exemplo com enumerações como campo.

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

Como eu disse, esse truque pode não funcionar em todos os casos, mesmo que struct tenha apenas campos de valor, somente você sabe que, se funcionar no seu caso ou não. apenas examine. mas você entendeu a ideia geral.

M.kazem Akhgary
fonte
Esta é uma boa solução para o exemplo que dei, mas na verdade deveria ser apenas um exemplo, a questão é geral.
Motti 27/09
2

Apenas caso especial. Se você vir um numerador de 0 e um denominador de 0, finja que ele tem os valores que você realmente deseja.

Jonathan Allen
fonte
5
Eu pessoalmente não gostaria que minhas classes / estruturas tivessem esse tipo de comportamento. Falhar silenciosamente (ou se recuperar da maneira que o desenvolvedor acha melhor) é o caminho para erros não detectados.
Boris Callens
2
+1 Esta é uma boa resposta, porque para os tipos de valor, é necessário levar em consideração o valor padrão. Isso permite que você "defina" o valor padrão com seu comportamento.
IllidanS4 quer Monica de volta 16/07/2015
É exatamente assim que eles implementam classes como Nullable<T>(por exemplo int?).
Jonathan Allen
Essa é uma péssima ideia. 0/0 deve sempre ser uma fração inválida (NaN). E se alguém ligar new Rational(x,y)onde xey são 0?
Mike Rosoft 23/07/19
Se você tiver um construtor real, poderá lançar uma exceção, impedindo que um 0/0 real aconteça. Ou, se você quiser que isso aconteça, é necessário adicionar um bool extra para distinguir entre padrão e 0/0.
Jonathan Allen
2

O que eu uso é o operador coalescente nulo (??) combinado com um campo de apoio como este:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

Espero que isto ajude ;)

Nota: a atribuição de coalescência nula é atualmente uma proposta de recurso para C # 8.0.

Axel Samyn
fonte
1

Você não pode definir um construtor padrão porque está usando C #.

As estruturas podem ter construtores padrão no .NET, embora eu não conheça nenhuma linguagem específica que a suporte.

Jonathan Allen
fonte
Em C #, classes e estruturas são semanticamente diferentes. Uma estrutura é um tipo de valor, enquanto uma classe é um tipo de referência.
precisa
-1

Aqui está minha solução para o dilema de construtor sem padrão. Sei que esta é uma solução tardia, mas acho que vale a pena notar que é uma solução.

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

ignorando o fato de eu ter uma estrutura estática chamada null, (Observação: isso é apenas para todos os quadrante positivo), usando get; set; em C #, você pode tentar / capturar / finalmente, para lidar com os erros em que um determinado tipo de dados não é inicializado pelo construtor padrão Point2D (). Eu acho que isso é evasivo como uma solução para algumas pessoas nesta resposta. Isso é principalmente porque eu estou adicionando o meu. O uso da funcionalidade getter e setter em C # permitirá ignorar esse construtor padrão sem sentido e tentar o que você não inicializou. Para mim, isso funciona bem; para outra pessoa, você pode querer adicionar algumas declarações if. Portanto, no caso em que você deseja uma configuração de Numerador / Denominador, esse código pode ajudar. Gostaria apenas de reiterar que esta solução não parece boa, provavelmente funciona ainda pior do ponto de vista de eficiência, mas, para alguém que vem de uma versão mais antiga do C #, o uso de tipos de dados de matriz oferece essa funcionalidade. Se você quer apenas algo que funcione, tente o seguinte:

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}
G1xb17
fonte
2
Este é um código muito ruim. Por que você tem uma referência de matriz em uma estrutura? Por que você simplesmente não tem as coordenadas X e Y como campos? E usar exceções para controle de fluxo é uma má idéia; você geralmente deve escrever seu código de forma que NullReferenceException nunca ocorra. Se você realmente precisa disso - embora essa construção seja mais adequada para uma classe do que para uma estrutura -, você deve usar a inicialização lenta. (E tecnicamente, você - completamente desnecessária em todos os mas a primeira definição de uma coordenada -. Definindo cada coordenada duas vezes)
Mike Rosoft
-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}
eMeL
fonte
5
É permitido, mas não é usado quando nenhum parâmetro é especificado. Ideone.com/xsLloQ
Motti