避免分配但不允许值类型使用默认零值

问题描述

C#中的值类型不能具有无参数的ctor,因为创建不带参数的实例时CLR的默认行为是将所有位设为零。

假设我有一个用例,我想拥有一个在基础值上强制某些不变式的类型,而这些不变式则禁止默认的全零状态。这迫使我使用引用类型。现在假设我赚了很多,我的意思是很多这种类型的实例。性能至关重要,这些分配加在一起,给GC带来很大压力。我非常想避免这些分配,因此首先想到使用值类型。但是a,我不能,因为默认值无效。

假定该类型满足值类型所需要的所有其他条件,即它确实表示一个值,具有值语义,最多占用16位。唯一的不足是,全零值是这种类型的无效状态。

我如何实现我的性能目标,而在上述情况下,该目标确实受到了GC压力的限制,而又不会牺牲我的类型所压缩的不变式?

编辑:

一个非常简单的例子,假设我拥有一个64位序列以及第一个非零位的索引。

public <struct/class> BitSequence64
{
    private long _bits;
    private int _firstNonZero;

    public IEnumerable<byte> Bytes => ...

    // A bunch of helper properties.

    public BitSequence64(long value)
    {
        // Set the _firstNonZero,etc.
        ...
    }

    // Methods that allow you to twiddle the bits but maintaining 
    // the invariant of always having at least one non-zero.
}

因此,显然将_bits设置为全零是没有意义的,尤其是因为_firstNonZero然后将指向第一位,即非零。这些序列很多,我非常希望这种类型的依赖项可以安全地使用它,而不必每次将其传递给面向公众的API时都无需验证其不是default值。

解决方法

我成功使用的一种技术是更改内部状态的解释方式,以使值类型default(T)的{​​{1}}在零无效或无效时有效。希望默认值为零。

究竟要如何实现,将取决于构成类型内部状态的因素。一般而言,成员字段中的至少一个将存储与消费者看到的值不同的值。取而代之的是,该类型的公共接口将说明进入和流出该类型的差异。

了解内部状态类型的自然属性(数学或其他方面)将帮助您确定如何做到这一点。

首先要做的是为该类型选择一个合理的默认值。显然,自然T被确定为不合理,因此需要选择其他内容。例如,这可能是有效范围内的最小值。但是,无论它是什么,它都会告诉您在存储输入之前如何调整输入,以及在返回它们之前如何调整内部值(通过逆运算)。

入门示例

以下default(T)包装器类型是该技术的一个非常人为的基本示例。

警告:请勿按原样使用此示例;它仅用于演示目的。

Year

此处,默认值为用于调整内部状态的public readonly struct Year { private const int Delta = 2000; private readonly int _value; public Year(int value) { _value = value - Delta; } public int Value => _value + Delta; } 常量。在Delta中,default(Year)将是_value,但是0属性将返回Value。同样,2000将在输入时将new Year(2000)转换为2000,并在输出时转换回0。另一种思考方式是2000代表与默认值的偏移量。

围绕此内部表示构建功能时,请务必记住只有构造函数和_value属性应访问后备字段。其他所有内容,甚至是私有成员,都应使用Value属性来确保一致性。同样,创建新实例应使用构造函数并传递面向消费者的值。在其他任何地方使用后备字段都会引发潜在的错误,因此最好避免这样做。单元测试对于确保一致性至关重要。

问题类型

Value的情况有点棘手,因为它具有特定的不变性。在注释中,看起来默认值为BitSequence64(仅设置了位0)可能是该类型的合理默认值。从现在开始,我将在假设的情况下进行操作。

这可以通过将实际值与1进行异或来实现。很好,因为XOR'ing by 1是它自己的逆运算。

现在,1是有效的,因为它表示的值也与default(BitSequence64)(也有效)所表示的值相同。

new BitSequence64(1L)

使用默认值public struct BitSequence64 { private const long DefaultBit = 1L; private long _value; private int _firstNonZero; public BitSequence64(long value) { if (value == 0) throw new ArgumentException("At least one bit must be set.",nameof(value)); _value = value ^ DefaultBit; _firstNonZero = GetFirstNonZero(_value); } public long Value => _value ^ DefaultBit; public int FirstNonZero => _firstNonZero; // Note that this property uses the post-adjustment,consumer-facing value. public IEnumerable<byte> Bytes => BitConverter.GetBytes(Value); private static int GetFirstNonZero(long value) { // TODO: Incorporate your implementation here. throw new NotImplementedException(); } // And,of course,let's not forget the members that do bit-twiddling // while maintaining the invariants. // ... } 很方便,但是如果您需要使用默认值1(设置了MSB)怎么办?默认为零时,0x1000_0000_0000_0000将不再正确。

在内部状态下解决这个问题很容易。我们可以将_firstNonZero重新定义为距位31“向下”的距离,而不是距位0“向上”的距离。除了按最高有效位对值进行XOR运算外,还修改构造函数和_firstNonZero属性以对FirstNonZero执行转换。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...