【问题标题】:Why can't I define a default constructor for a struct in .NET?为什么我不能在 .NET 中为结构定义默认构造函数?
【发布时间】:2010-09-24 22:22:16
【问题描述】:

在 .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 个非默认 Rationals 的容器(并且想要优化掉 1000 个结构),我将使用 List<Rational> 而不是数组。

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

【问题讨论】:

  • +1曾经遇到过类似的问题,终于把struct转成class了。
  • 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# .net struct


【解决方案1】:

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

结构体是一个值类型,一个值类型一被声明就必须有一个默认值。

MyClass m;
MyStruct m2;

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

【讨论】:

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

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

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

并像这样使用它:

var rat = Rational.One;

【讨论】:

  • 在这种情况下,Rational.Zero 可能不会那么混乱。
【解决方案4】:

简短的解释:

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

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

【讨论】:

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

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

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

正如我所说,这个技巧可能不适用于所有情况,即使结构只有值字段,只有你知道它是否适用于你的情况。只是检查。但你明白了。

【讨论】:

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

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

【讨论】:

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

我使用的是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 的功能提议。

【讨论】:

    【解决方案8】:

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

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

    【讨论】:

    • 在 C# 中,类和结构在语义上是不同的。结构是值类型,而类是引用类型。
    【解决方案9】:

    【讨论】:

    • 从我在您发布的链接中看到的内容,它只会在显式调用构造函数时被激活,而不是在通过default 或在数组中创建时被激活......哎呀。
    【解决方案10】:

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

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

    【讨论】:

      【解决方案11】:

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

      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 的方式编写代码。如果你真的需要这个——尽管这样的构造更适合类而不是结构——那么你应该使用延迟初始化。 (从技术上讲,除了第一个坐标设置之外,您完全没有必要将每个坐标设置两次。)
      【解决方案12】:
      public struct Rational 
      {
          private long numerator;
          private long denominator;
      
          public Rational(long num = 0, long denom = 1)   // This is allowed!!!
          {
              numerator   = num;
              denominator = denom;
          }
      }
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-07-23
      • 1970-01-01
      • 2020-04-23
      • 2019-05-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多