C# - 我该如何确保所有的结构都被初始化?

6

我正在使用C#编写一个应用程序,需要进行大量计算。所有计算围绕基本结构Value展开。它基本上是带有一些附加参数(精度等)的double。

必须使用结构体,因为将创建太多的实例以至于无法承受堆分配。

现在,我需要确保它们都正确地初始化。

尽管我提供了默认构造函数,但我不能声明默认的显式构造函数。默认构造函数会将所有内容初始化为0,在我的领域中没有意义。

那么,有没有办法拒绝创建实例而不调用我的构造函数呢?

基本上,我需要使这个测试通过:

[Test]
public void HowDoesThisStructureInitializeByDefault()
{
   Value v = new Value(); - if this did not compile - it would have been ok!

   Assert.AreEqual(0, v.Val); - passes
   Assert.AreEqual(-1, v.Accuracy); - fails
}

如果没有显式调用构造函数并且仍在访问结构,则抛出异常是可以的,但始终检查会花费太多时间。

我现在几乎失去了希望,请帮忙!


我知道你并没有询问这个,但我很难相信你真的需要使用 struct 来提高性能。你应该尽可能以最清晰、最简单的方式编写代码,然后在优化之前进行分析。 - Jay Bazuzi
5个回答

4
为什么你不能定义一个显式的构造函数?这就是它们的作用所在。此外,为什么你认为“你负担不起堆分配”?在托管语言中,堆分配非常便宜。你如何测试这个假设,即你负担不起堆分配,或者说堆分配本身更加昂贵?
(对于一个由“双精度浮点数和若干参数”组成的类型,我怀疑你已经达到了一个堆分配更便宜、更高效的大小)
无论如何,如果用户希望这样做,你无法阻止他调用值类型的默认构造函数。你所能做的只是确保存在一种更好的初始化值的方法,例如非默认构造函数,或者如果由于某种原因无法创建,则可以在调用时创建和初始化值类型的函数。
但是当然,你无法保证人们实际上会调用它。
(编辑:) 在.NET中,堆分配基本上只包括一个简单的堆栈推送操作。这就是托管(和垃圾收集)语言的好处。运行时本质上使用一个大堆栈作为其堆,因此每次分配只需将堆栈指针增加一些(当然,在检查是否有足够的空闲内存之后)。然后垃圾收集器在必要时负责压缩内存。
因此,堆分配本身非常便宜。当然,额外的GC压力可能会再次减慢速度(尽管据我所知,GC传递所需的时间仅取决于活动对象的数量,而不是要进行GC的对象,因此拥有无数“死”对象可能并不是一个大问题),但另一方面,栈分配也不是免费的。值类型按值传递,因此每次将你的类型作为参数传递给函数或返回它时,都必须进行一次复制。我不知道你的值有多大,但一个双精度浮点数占8个字节,假设有其他几个参数,我会认为是32个字节。这可能太大了,以至于valuetype需要额外复制,使得它比使用堆分配更慢。也许。
如你所见,值类型和引用类型都有优势。我不能说哪种在你的情况下更快,但如果我是你,我会非常小心地做出这种假设。如果可能的话,构建你的代码,以便你可以在引用类型和值类型实现之间切换,并查看哪个效果最好。或者,编写较小的测试,尝试预测每种方法在大规模情况下的表现。

1
@jalf - 1) 我无法定义显式构造函数,因为C#不允许在结构体上使用显式构造函数。2) 我认为堆分配更加昂贵,因为你需要调用某些东西来完成它。你甚至可能会遇到OutOfMemoryException。对于栈分配,你不需要做任何事情,我错了吗? - Paul Kapustin
哦,我认为您所说的显式构造函数是指非默认构造函数。定义一个带参数的构造函数,并使用这些参数来初始化结构体,然后始终使用它。 我将编辑我的帖子,以更详细地说明堆分配的成本,请检查一下。 :) - jalf
@jalf - 谢谢。现在很有意义。不过,我正在想象用C++编写这种代码。你有一个数组Values,你会声明一个常规数组(堆栈)还是指针数组(堆)?这不仅仅是关于分配的问题。如果使用堆,每次都需要通过引用找到对象。 - Paul Kapustin
是的,在C++中,我肯定会同意堆栈分配。而且确实,引用堆上的对象涉及指针间接和可能的缓存未命中。是否足以使堆栈分配更有效只能通过测试来回答。 - jalf

3
你无法摆脱默认构造函数 (Jon Skeet 在 为什么.NET中的结构体不能定义默认构造函数? 中回答得非常好),但你可以创建一个工厂类,以允许使用正确初始化的参数定义结构值。您可以使用带有模拟/验证的单元测试来确保代码创建新值时使用工厂。这将是一种约定,需要由您执行,因为编译器不会为您强制执行它。
public static class StructFactory
{
    public static Value DefaultValue()
    {
         Value v = new Value();
         v.Value = 0.0;
         v.Accuracy = 15; /* digits */
         return v;
    }
}

...

Value v = StructFactory.DefaultValue();

@tvanfosson "你可以使用具有模拟/验证功能的单元测试来确保当您的代码创建新值时,它们使用工厂" - 你如何确保每个人都使用工厂?而不进行特殊检查结构(这很昂贵?) - Paul Kapustin
对于你编写的代码,你可以模拟工厂并在单元测试中设置期望,以确保工厂方法被调用。重点是这是一个约定,必须在编译器之外强制执行。我还会在代码中进行文档记录,以便使用该结构的人了解如何操作。 - tvanfosson

3

结构体字段被初始化为零(或null,或一般的default(T))。

如果你希望Accuracy的初始值为-1,你可以实现Accuracy属性,使得当基础字段==0时,该属性返回-1。

一个可能的方法:

struct Value
{
  int _accuracyPlusOne;

  public int Accuracy
  { 
    get { return _accuracyPlusOne - 1; }
    get { _accuracyPlusOne= value + 1; }
  }
}

1
我真的无法确定这个解决方案是可怕的还是聪明的。 - Robert Rossney
@Robert:实际上,这并不是;相反,这是一种常规的实现方式。System.DateTime也实现了类似的功能。 - Konrad Rudolph

2

这实际上是核心CLR的限制,而不是C#。 - JaredPar
Jared:这是一个混合体。IL确实允许您定义一个无参数构造函数(甚至是非公共的),如果您在C#中编写new Value(),它将被调用-但在初始化数组时不会被调用。我对第一部分也感到非常惊讶... - Jon Skeet

0

你希望使用默认构造函数将精度初始化为-1吗?我认为你无法阻止其他人使用new Value(),但是你可以添加一个构造函数,让你可以使用new Value(10)并且以你想要的方式初始化精度。

请参阅MSDN页面有关[结构体构造函数](http://msdn.microsoft.com/en-us/library/aa288208(VS.71).aspx)的内容。

如果你遇到精度为0的情况(如果该值从未有意义),你总是可以在使用你的结构体的代码中抛出异常。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接