290

在 .NET 中,值类型 (C# struct) 不能有没有参数的构造函数。根据这篇文章,这是 CLI 规范强制要求的。发生的情况是,对于每个值类型,都会创建一个默认构造函数(由编译器?),它将所有成员初始化为零(或null)。

为什么不允许定义这样的默认构造函数?

一个微不足道的用途是有理数:

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;
    }
}

使用当前版本的 C#,默认的 Rational0/0不是那么酷。

PS:默认参数会帮助 C# 4.0 解决这个问题,还是会调用 CLR 定义的默认构造函数?


Jon Skeet回答:

以您的示例为例,当有人这样做时,您希望发生什么:

 Rational[] fractions = new Rational[1000];

它应该通过您的构造函数运行 1000 次吗?

当然应该,这就是我首先编写默认构造函数的原因。当未定义显式默认构造函数时,CLR 应使用默认归零构造函数;这样你只需为你使用的东西付费。然后,如果我想要一个包含 1000 个非默认Rationals 的容器(并且想要优化掉 1000 个结构),我将使用 aList<Rational>而不是数组。

在我看来,这个原因不足以阻止定义默认构造函数。

4

12 回答 12

211

注意:下面的答案是在 C# 6 之前很久写的,它计划引入在结构中声明无参数构造函数的能力 - 但它们仍然不会在所有情况下(例如用于数组创建) (最后此功能未添加到 C# 6 中)。


编辑:由于 Grauenwolf 对 CLR 的深入了解,我编辑了下面的答案。

CLR 允许值类型具有无参数构造函数,但 C# 不允许。我相信这是因为它会引入一个期望,即构造函数会在它不会被调用时被调用。例如,考虑一下:

MyStruct[] foo = new MyStruct[1000];

只需分配适当的内存并将其全部归零,CLR 就能够非常有效地做到这一点。如果它必须运行 MyStruct 构造函数 1000 次,那么效率会低很多。(事实上​​,它没有——如果你一个无参数的构造函数,当你创建一个数组或者你有一个未初始化的实例变量时它不会运行。)

C# 中的基本规则是“任何类型的默认值都不能依赖于任何初始化”。现在他们可以允许定义无参数的构造函数,但不需要在所有情况下都执行该构造函数——但这会导致更多的混乱。(或者至少,所以我相信这个论点。)

编辑:以您的示例为例,当有人这样做时,您希望发生什么:

Rational[] fractions = new Rational[1000];

它应该通过您的构造函数运行 1000 次吗?

  • 如果不是,我们最终会得到 1000 个无效的有理数
  • 如果是这样,那么如果我们要用实际值填充数组,我们可能会浪费大量工作。

编辑:(回答更多问题)无参数构造函数不是由编译器创建的。就 CLR 而言,值类型不必具有构造函数——尽管如果你用 IL 编写它,事实证明它可以。当您new Guid()在 C# 中编写“”时,它会发出与调用普通构造函数不同的 IL。有关这方面的更多信息,请参阅这个 SO question 。

怀疑框架中没有任何带有无参数构造函数的值类型。毫无疑问,如果我问得足够好,NDepend 可以告诉我...... C# 禁止它的事实足以让我认为这可能是一个坏主意。

于 2008-12-02T12:48:21.920 回答
54

结构是值类型,值类型在声明后必须具有默认值。

MyClass m;
MyStruct m2;

如果您像上面那样声明两个字段而没有实例化任何一个,然后中断调试器,m它将为 null 但m2不会。鉴于此,无参数构造函数将毫无意义,事实上,结构上的所有构造函数所做的都是赋值,事物本身已经存在,只是通过声明它。实际上,m2 可以很高兴地用于上面的示例,并且可以调用它的方法(如果有的话),并操纵它的字段和属性!

于 2008-12-02T16:58:28.093 回答
20

您可以创建一个静态属性来初始化并返回默认的“有理数”:

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

并像这样使用它:

var rat = Rational.One;
于 2009-01-11T17:14:33.383 回答
14

简短的解释:

在 C++ 中,struct 和 class 只是一枚硬币的两个方面。唯一真正的区别是默认情况下一个是公共的,另一个是私有的。

.NET中,结构和类之间的区别要大得多。最主要的是 struct 提供值类型语义,而 class 提供引用类型语义。当您开始考虑此更改的含义时,其他更改也开始变得更有意义,包括您描述的构造函数行为。

于 2008-12-02T14:03:56.293 回答
3

我还没有看到相当于我要给出的后期解决方案,所以就在这里。

使用偏移量将值从默认 0 移动到您喜欢的任何值。这里必须使用属性而不是直接访问字段。(也许使用可能的 c#7 功能,您可以更好地定义属性范围字段,以便它们受到保护,不会在代码中被直接访问。)

此解决方案适用于仅具有值类型(无 ref 类型或可为空的结构)的简单结构。

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;
    }
}

这与这个答案不同这种方法不是特殊的大小写,而是它使用的偏移量适用于所有范围。

以枚举作为字段的示例。

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;
    }
}

正如我所说,这个技巧可能并非在所有情况下都有效,即使 struct 只有值字段,也只有您知道它是否适用于您的情况。只是检查。但你明白了。

于 2017-09-27T17:31:39.760 回答
2

只是特例而已。如果您看到分子为 0,分母为 0,请假装它具有您真正想要的值。

于 2008-12-03T07:59:06.963 回答
2

我使用的是空合并运算符 (??)与这样的支持字段相结合:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

希望这可以帮助 ;)

注意:null 合并分配当前是 C# 8.0 的功能建议。

于 2019-11-20T10:09:41.010 回答
1

您无法定义默认构造函数,因为您使用的是 C#。

结构可以在 .NET 中具有默认构造函数,但我不知道任何支持它的特定语言。

于 2008-12-03T19:53:48.893 回答
1

从 C# 10.0 开始,您可以:

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#parameterless-constructors-and-field-initializers

于 2021-09-21T13:47:59.080 回答
1

我为此找到了简单的解决方案:

struct Data
    {
        public int Point { get; set; }
        public HazardMap Map { get; set; }
        public Data Initialize()
        {
            Point = 1; //set anything you want as default
            Map = new HazardMap();
            return this;
        }
    }

在代码中只做:

Data input = new Data().Initialize();
于 2021-12-05T21:47:51.540 回答
-1

这是我对无默认构造函数困境的解决方案。我知道这是一个较晚的解决方案,但我认为值得注意的是这是一个解决方案。

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 );
    }
}

忽略我有一个名为 null 的静态结构的事实,(注意:这仅适用于所有正象限),使用 get;set; 在 C# 中,您可以使用 try/catch/finally 来处理默认构造函数 Point2D() 未初始化特定数据类型的错误。我想这对于某些人来说是难以捉摸的解决方案。这主要是为什么我要添加我的。在 C# 中使用 getter 和 setter 功能将允许您绕过这个默认的构造函数,并尝试捕获您未初始化的内容。对我来说这很好用,对于其他人你可能想要添加一些 if 语句。因此,如果您需要分子/分母设置,此代码可能会有所帮助。我想重申一下,这个解决方案看起来不太好,从效率的角度来看可能效果更差,但是,对于来自旧版 C# 的人来说,使用数组数据类型可以为您提供此功能。如果你只是想要一些有用的东西,试试这个:

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;
    }
}
于 2016-11-13T19:20:12.853 回答
-2
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}
于 2018-08-17T18:23:23.630 回答