使用受限泛型代替接口——有哪些缺点?

8

假设我有

interface IMatrix {
    double this[int r, int c] { get; }
}

struct Matrix2x2 : IMatrix  {
    double a1, a2, b1, b2;
    double this[int r, int c] { get { ... } }
}

struct Matrix3x3 : IMatrix {
    double a1, a2, a3, b1, b2, b3, c1, c2, c3;
    double this[int r, int c] { get { ... } }
}

class Matrix : IMatrix {    // Any size
    double[,] cells;
    double this[int r, int c] { get { ... } }
}

有时候,我们不仅需要说出来:

static class Matrices {
    static IMatrix Multiply(IMatrix a, IMatrix b) { ... }
}

我最终做了
static class Matrices {
    static IMatrix Multiply<T1, T2>(T1 a, T2 b)
        where T1 : IMatrix
        where T2 : IMatrix { ... }
}

或者甚至更多。
static class Matrices {
    static IMatrix Multiply<T1, T2>([In] ref T1 a, [In] ref T2 b)
        where T1 : IMatrix
        where T2 : IMatrix { ... }
}

为避免结构体的装箱或复制。
这种方法运行良好,但是否存在我不知道的任何缺点(除了内存使用量的微不足道的增加)?这是一种被接受的做法,还是因为某些原因而被反对,我可能没有意识到?

像我在帖子中提到的那样,这是为了“避免装箱或复制结构体”,因为它们是相对昂贵的操作。 - user541686
2个回答

4

泛型会带来一些成本,主要是代码大小的增加。Joe Duffy最近的一篇博客详细介绍了这个问题。然而,通常情况下,避免频繁调用代码中的装箱操作是件好事,可能值得生成更多的字节码(实际上,这意味着稍微增加一些内存使用和JIT的工作量)。


+1 我从未知道“一个空类型意味着1K的内存”,很高兴知道!感谢您提供文章链接。 - user541686
为什么会导致更大的二进制文件?每个通用方法只出现一次。 - svick
@svick:抱歉,你是对的,这更多是运行时代码生成(对JIT来说更多工作)。我已经纠正了我的回答。 - bobbymcr

1

接口约束非常好,但有一个重要的注意事项:虽然结构可以具有不可变语义、可变值语义或可变引用语义,但如果是可变的装箱结构,则始终具有引用语义。尽管可变值语义通常很有用,但在泛型中很难使它们与之良好地协作。甚至考虑一个问题,即您列出的矩阵乘积的参数应该按引用还是按值传递:如果泛型类型是值类型,则参数可能应该按引用传递;如果是类类型,则应该按值传递。

作为引用/值区别的另一个例子,假设一个方法有一个矩阵,并且它想要的是一个与旧矩阵完全相同但元素(0,0)和(1,1)为零的矩阵,并且它不再需要原始矩阵。如果矩阵是一个可变值类型,具有可设置的索引属性,则该方法可以简单地写入元素(0,0)和(1,1),而不会产生不必要的副作用;如果它是一个可变引用类型,则该例程可能首先需要进行防御性克隆(ick);如果它是一个不可变类型,则每次修改都可能需要创建一个新实例(ick)。使用值类型可以提供更清晰的语义,但如果系统执行像装箱这样的代码转换,将值类型语义转换为引用语义或损坏的语义,就可能会导致意外的副作用。
(*) 一个简单公开可变字段的结构体表现出非常干净的可变值类型语义。给定以下声明: public struct valueStruct {public int value; ... } public interface IManipulateValueStruct {public void manipulate(ref valueStruct it);} public void someMethod(IManipulateValueStruct manipulator1, manipulator2) { valueStruct x,y; ... manipulator1.Manipulate(ref x); manipulator2.Manipulate(ref y); // 这个方法对x和y做了什么? ... }

仅仅看上述代码,我们可以确定指示的方法调用可能会影响y.value,但不会影响x.value,也不会在方法调用返回后任何时候导致y.value改变。相比之下,如果valueStruct是一个类,那么无法确定指示的调用会对x.value做什么,也无法确定它是否会在某个未来的任意时间导致y.value改变。

尽管具有公开字段的结构实现了可变值语义,但如果它们持有不可变的类类型字段,并且只公开作用于该字段的修改器,则结构体可以实现可变引用语义。这有时可能是一种有用的模式,但受到限制,因为C#和vb编译器将禁止类似于可能试图改变结构体(实际上会改变其所持有的类对象)的操作。请注意,具有可变引用语义的不可变结构必须通过非默认构造函数进行初始化才能使用。通过默认初始化创建的这些结构通常是无效和无用的;充其量它们将无法区分彼此。


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