ChatGPT解决这个技术问题 Extra ChatGPT

为什么我不能在 .NET 中为结构定义默认构造函数?

在 .NET 中,值类型 (C# struct) 不能有没有参数的构造函数。根据 this post,这是 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#,默认的 Rational 是 0/0,这不是很酷。

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

Jon Skeet 回答:

以您的示例为例,当有人这样做时,您希望发生什么: Rational[] fractions = new Rational[1000];它应该通过您的构造函数运行 1000 次吗?

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

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

+1曾经有过类似的问题,最后将结构转换为类。
C#4 中的默认参数无济于事,因为 Rational() 调用无参数 ctor 而不是 Rational(long num=0, long denom=1)
请注意,在 Visual Studio 2015 附带的 C# 6.0 中,将允许为结构编写零参数实例构造函数。因此,如果构造函数存在,new Rational() 将调用它,但如果它不存在,则 new Rational() 将等效于 default(Rational)。在任何情况下,当您想要结构的“零值”(对于您建议的 Rational 设计而言,这是一个“坏”数字)时,我们鼓励您使用语法 default(Rational)。值类型 T 的默认值始终为 default(T)。所以 new Rational[1000] 永远不会调用结构构造函数。
要解决这个特定问题,您可以将 denominator - 1 存储在结构中,以便默认值变为 0/1
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. 为什么您希望数组为结构调用与 List 不同的构造函数?

C
Community

注意:下面的答案是在 C# 6 之前很久写的,它计划引入在结构中声明无参数构造函数的能力 - 但它们仍然不会在所有情况下都被调用(例如用于创建数组)(最后是这个特性was not added to C# 6)。

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

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

MyStruct[] foo = new MyStruct[1000];

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

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

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

Rational[] fractions = new Rational[1000];

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

如果不是,我们最终会得到 1000 个无效的有理数

如果是这样,那么如果我们要用实际值填充数组,我们可能会浪费大量工作。

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

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


更简短的解释:在 C++ 中,结构和类只是同一枚硬币的两个方面。唯一真正的区别是一个是默认公开的,另一个是私有的。在 .Net 中,结构和类之间的区别要大得多,理解它很重要。
@Joel:但这并不能真正解释这个特殊的限制,是吗?
CLR 确实允许值类型具有无参数构造函数。是的,它将为数组中的每个元素运行它。 C# 认为这是一个坏主意并且不允许这样做,但是您可以编写一个 .NET 语言来做到这一点。
对不起,我对以下内容有点困惑。如果 Rational 是类而不是结构,Rational[] fractions = new Rational[1000]; 是否也会浪费大量工作?如果是这样,为什么类有一个默认的ctor?
@FifaEarthCup2014:您必须更具体地说明“浪费大量工作”的含义。但是无论哪种方式,它都不会调用构造函数 1000 次。如果 Rational 是一个类,您将得到一个包含 1000 个空引用的数组。
T
Tarik

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

MyClass m;
MyStruct m2;

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


不知道为什么有人投票给你。您似乎是这里最正确的答案。
C++ 中的行为是,如果一个类型有一个默认构造函数,那么在没有显式构造函数的情况下创建这样的对象时使用默认构造函数。这可以在 C# 中使用默认构造函数初始化 m2,这就是为什么这个答案没有帮助。
onester:如果您不希望结构在声明时调用自己的构造函数,那么不要定义这样的默认构造函数! :) 这是莫蒂的说法
@塔里克。我不同意。相反,无参数构造函数将完全有意义:如果我想创建一个“矩阵”结构,它总是有一个单位矩阵作为默认值,你怎么能通过其他方式做到这一点?
not sure I fully agree同意“确实可以很高兴地使用 m2..”。在以前的 C# 中可能是这样,但是声明一个结构而不是 new 它是编译器错误,然后尝试使用它的成员
A
AustinWBryan

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

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

并像这样使用它:

var rat = Rational.One;

在这种情况下,Rational.Zero 可能不那么令人困惑。
P
Peter Mortensen

简短的解释:

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

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


您必须更明确地说明值与引用类型拆分是如何暗示的,我不明白...
值类型有一个默认值——它们不为空,即使您没有定义构造函数。虽然乍一看这并不排除定义默认构造函数,但框架内部使用此功能对结构做出某些假设。
@annakata:其他构造函数在某些涉及反射的场景中可能很有用。此外,如果泛型被增强以允许参数化的“新”约束,那么拥有可以符合它们的结构将会很有用。
@annakata 我相信这是因为 C# 有一个特别强烈的要求,即必须编写 new 才能调用构造函数。在 C++ 中,构造函数在数组的声明或实例化时以隐藏的方式调用。在 C# 中,要么一切都是指针,所以从 null 开始,要么它是一个结构,并且必须从某个东西开始,但是当你不能写 new...(如数组 init)时,这会破坏一个强大的 C# 规则。
a
asaf92

从 C# 10.0 开始,您可以:

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


从我在您发布的链接中看到的内容,它只会在显式调用构造函数时被激活,而不是在通过 default 或在数组中创建时被激活......哎呀。
M
M.kazem Akhgary

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

使用偏移量将值从默认 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;
    }
}

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

以枚举作为字段的示例。

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 只有值字段,也只有您知道它是否适用于您的情况。只是检查。但你明白了。


对于我给出的示例来说,这是一个很好的解决方案,但它实际上只是一个示例,问题很笼统。
J
Jonathan Allen

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


我个人不希望我的类/结构有这种行为。默默地失败(或以开发人员猜测的最适合你的方式恢复)是通往未发现错误的道路。
+1 这是一个很好的答案,因为对于值类型,您必须考虑它们的默认值。这让您可以通过其行为“设置”默认值。
这正是他们实现诸如 Nullable<T>(例如 int?)之类的类的方式。
这是一个非常糟糕的主意。 0/0 应始终是无效分数 (NaN)。如果有人在 x 和 y 恰好为 0 的情况下调用 new Rational(x,y) 怎么办?
如果你有一个实际的构造函数,那么你可以抛出一个异常,防止真正的 0/0 发生。或者,如果您确实希望它发生,则必须添加一个额外的布尔值来区分默认值和 0/0。
P
Pang

我使用的是 null-coalescing operator (??) 与这样的支持字段相结合:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

希望这可以帮助 ;)

注意:null coalescing assignment 目前是针对 C# 8.0 的功能提议。


J
Jonathan Allen

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

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


在 C# 中,类和结构在语义上是不同的。结构是值类型,而类是引用类型。
r
rekaha

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

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

G
G1xb17

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

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

这是非常糟糕的代码。为什么结构中有数组引用?为什么不简单地将 X 和 Y 坐标作为字段?并且使用异常进行流控制是一个坏主意;您通常应该以永远不会发生 NullReferenceException 的方式编写代码。如果你真的需要这个——尽管这样的构造更适合类而不是结构——那么你应该使用延迟初始化。 (从技术上讲,除了坐标的第一个设置之外,您完全没有必要将每个坐标设置两次。)
e
eMeL
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}

允许,但未指定参数时不使用ideone.com/xsLloQ