有没有办法在定义它的 class 中引入对泛型类型的约束?

Is there a way to introduce a constraint to a generic type inside the class defining it?

我有一个内存块的接口,应该通过 class 管理 ram 内存和 class 管理磁盘上的内存块来实现。第一个 class 应该也支持引用类型,而第二个只支持值类型。

至于接口示例,请考虑以下内容。 我没有为 T 设置约束以允许支持第一个 class:

的引用类型
public interface IMemoryBlock<T>
{
    T this[int index] {get; }
}

第二个 class 在初始化时检查 T 是否为值类型 (typeof(T).IsValueType),应该具有如下内容:

public class DiskMemoryBlock<T> : IMemoryBlock<T>
{
    public T this[int index]
    {
        get
        {
            byte[] bytes = ReadBytes(index * sizeOfT, sizeOfT);
            return GenericBitConverter.ToValue<T>(bytes, 0);
        }
    }
}

这不起作用,因为 GenericBitConverter.ToValue<T> 需要 T 上的值类型约束。有没有办法稍后引入约束来解决这种情况?否则,在这种情况下,您有什么建议与编写没有约束的自定义 GenericBitConverter.ToValue<T> 不同?

编辑

我忘了说明我有一个 Buffer<T> class,根据参数应该初始化 DiskMemoryBlock<T>RamMemoryBlock<T>:

public class Buffer<T>
{
    IMemoryBlock<T> buffer;
    public Buffer(**params**)
    {
        buffer = (**conditions**) ? new DiskMemoryBlock<T>() : new RamMemoryBlock<T>();
    }
}

只要你想让一个DiskMemoryBlock只包含值类型,你可以直接约束它:

class DiskMemoryBlock<T> : IMemoryBlock<T> where T : struct

这是泛型编程中的一种常见模式:您使用不受约束的类型参数定义通用接口,当您下降 class 层次结构时,这些接口会随着 subclasses 附加约束而变得更加细化。

当您创建一个DiskMemoryBlock时,类型检查器将尝试解决约束。

new DiskMemoryBlock<int>();  // ok
new DiskMemoryBlock<string>();  // type error

如果您希望 DiskMemoryBlock 能够包含引用类型,那么,您不能使用 GenericBitConverter.ToValue<T>

@Benjamin Hodgson 你问题的主要部分和我一样,所以我不会浪费 space 重复他。这只是对您的编辑的回应。

您想根据来自 Buffer<T> 构造函数参数的某些条件实例化 DiskMemoryBlock<T>RamMemoryBlock<T>

public class Buffer<T>
{
    IMemoryBlock<T> buffer;
    public Buffer(**params**)
    {
        buffer = (**conditions**) ? new DiskMemoryBlock<T>() : new RamMemoryBlock<T>();
    }
}

不幸的是,在实例化 class 的对象时(忽略反射 jiggery-pokery),class 上的任何类型约束都需要在编译时得到保证。这意味着您可以构造 DiskMemoryBlock<T> 的唯一方法是:

  1. 直接将T指定为值类型(例如var block = new DiskMemoryBlock<int>()
  2. 在具有 struct 约束的上下文中构造它。

你的情况不是这两种情况。鉴于拥有通用 Buffer 似乎很重要,选项 1 没有任何帮助,因此选项 2 是您唯一的选择。

让我们看看是否可以解决这个问题。

我们可以尝试把困难的IMemoryBlock创建放到一个虚方法中,创建一个Buffer的value-type-only子class,它可以创建一个DiskMemoryBlock 没有问题:

public class Buffer<T>
{
    IMemoryBlock<T> buffer;

    public Buffer(**params**)
    {
        buffer = (**conditions**) 
            ? CreateMemoryBlockPreferDisk() 
            : new RamMemoryBlock<T>();
    }

    protected virtual IMemoryBlock<T> CreateMemoryBlockPreferDisk()
    {
        return new RamMemoryBlock<T>(); 
    }
}

public class ValueBuffer<T> : Buffer<T> where T : struct
{
    public ValueBuffer(**params**) : base(**params**) { }

    protected override IMemoryBlock<T> CreateMemoryBlockPreferDisk()
    {
        return new DiskMemoryBlock<T>();
    }
}

好的,所以我们有一些可行的东西,但是有一点问题 - 从构造函数调用虚拟方法不是一个好主意 - 派生的 class' 构造函数没有 运行 然而,所以我们可以在 ValueBuffer 中进行各种未初始化的事情。在这种情况下没关系,因为覆盖不使用派生 class 的任何成员(事实上,没有任何成员),但如果有的话,它会为将来意外破坏打开大门还有没有子classes.

所以也许不是让基 class 调用派生 class,我们可以让派生 class 将函数传递给基 class?

public class Buffer<T>
{
    IMemoryBlock<T> buffer;

    public Buffer(**params**)
        : this(**params**, () => new RamMemoryBlock<T>())
    {
    }

    protected Buffer(**params**, Func<IMemoryBlock<T>> createMemoryBlockPreferDisk)
    {
        buffer = (**conditions**)
            ? createMemoryBlockPreferDisk()
            : new RamMemoryBlock<T>();
    }
}

public class ValueBuffer<T> : Buffer<T>
    where T : struct
{
    public ValueBuffer(**params**)
        : base(**params**, () => new DiskMemoryBlock<T>())
    {
    }
}

这看起来更好 - 没有虚拟电话,一切都很好。虽然...

我们遇到的问题是在 运行 时试图在 DiskMemoryBlockRamMemoryBlock 之间做出选择。我们已经解决了这个问题,但现在如果您想要 Buffer,则必须在 BufferValueBuffer 之间做出选择。没关系 - 我们可以一直这样做,对吧?

好吧,我们可以,但那是为每个 class 一直创建两个版本。这是很多工作和痛苦。如果还有第三个约束 - 一个只处理引用类型的缓冲区,或者一个特殊的 space 高效 bool 缓冲区怎么办?

解决方案类似于将 createMemoryBlockPreferDisk 传递给 Buffer 的构造函数的方法 - 让 Buffer 完全不知道它正在使用的 IMemoryBlock 的类型,并给它一个函数,该函数将为它创建相关类型。更好的是,将函数包装在工厂 class 中,以防我们以后需要更多选项:

public enum MemoryBlockCreationLocation
{
    Disk,
    Ram
}

public interface IMemoryBlockFactory<T>
{
    IMemoryBlock<T> CreateMemoryBlock(MemoryBlockCreationLocation preferredLocation);
}

public class Buffer<T>
{
    IMemoryBlock<T> buffer;

    public Buffer(**params**, IMemoryBlockFactory<T> memoryBlockFactory)
    {
        var preferredLocation = (**conditions**)
            ? MemoryBlockCreationLocation.Disk
            : MemoryBlockCreationLocation.Ram;

        buffer = memoryBlockFactory.CreateMemoryBlock(preferredLocation);
    }
}

public class GeneralMemoryBlockFactory<T> : IMemoryBlockFactory<T>
{
    public IMemoryBlock<T> CreateMemoryBlock(MemoryBlockCreationLocation preferredLocation)
    {
        // We can't create a DiskMemoryBlock in general - ignore the preferred location and return a RamMemoryBlock.
        return new RamMemoryBlock<T>();
    }
}

public class ValueTypeMemoryBlockFactory<T> : IMemoryBlockFactory<T>
    where T : struct
{
    public IMemoryBlock<T> CreateMemoryBlock(MemoryBlockCreationLocation preferredLocation)
    {
        switch (preferredLocation)
        {
            case MemoryBlockCreationLocation.Ram:
                return new RamMemoryBlock<T>();

            case MemoryBlockCreationLocation.Disk:
            default:
                return new DiskMemoryBlock<T>();
        }
    }
}

我们仍然需要在某个地方决定我们需要哪个版本的 IMemoryBlockFactory,但是如上所述,没有办法解决这个问题——类型系统需要知道您使用的是哪个版本的 IMemoryBlock在编译时实例化。

从好的方面来说,该决定与您的 Buffer 之间的所有 class 只需要知道 IMemoryBlockFactory 的存在。这意味着您可以改变事物并保持涟漪效应相当小:

  • 如果您需要额外的 IMemoryBlock 类型,您可以创建一个额外的 IMemoryBlockFactory class 就可以了。
  • 如果您需要根据更复杂的因素来决定 IMemoryBlock 类型,您可以更改 CreateMemoryBlock 以采用不同的参数,只有工厂和 Buffer 实施受到影响。

如果你不需要这些优势(内存块不太可能改变并且你倾向于实例化 Buffer 具有具体类型的对象),那么无论如何,不​​要为额外的而烦恼拥有工厂的复杂性,并使用这个答案中途的版本(Func<IMemoryBlock> 被传递到构造函数中)。