C#中正确的可空类型检查是什么?

4

好的,我的实际问题是这样的:我正在实现一个 IList<T>。当我到达 CopyTo(Array array, int index) 时,这是我的解决方案:

void ICollection.CopyTo(Array array, int index)
{
    // Bounds checking, etc here.
    if (!(array.GetValue(0) is T))
        throw new ArgumentException("Cannot cast to this type of Array.");
    // Handle copying here.
}

这在我的原始代码中有效,现在仍然有效。但它有一个小缺陷,在我开始为它构建测试时才暴露出来,特别是这个:

public void CopyToObjectArray()
{
    ICollection coll = (ICollection)_list;
    string[] testArray = new string[6];

    coll.CopyTo(testArray, 2);
}

现在,这个测试应该通过了。它抛出了ArgumentException,说明无法转换。为什么呢?因为 array[0] == null。当检查一个被设置为null的变量时,is关键字总是返回false。现在,这对于各种原因都非常方便,包括避免空指针引用等。我最终想到的类型检查方法是:

try
{
    T test = (T)array.GetValue(0);
}
catch (InvalidCastException ex)
{
    throw new ArgumentException("Cannot cast to this type of Array.", ex);
}

这并不是很优雅,但它可以运行... 不过有更好的方法吗?

4个回答

4

针对此问题,Type上有一个特定的方法可供使用,尝试一下:

if(!typeof(T).IsAssignableFrom(array.GetElementType()))

使用反射肯定比直接进行无效转换要更耗费资源。 - Jeff Yates
对于努力的人,我会给予+1的评价,但就性能而言,我认为我必须站在ffpf这边,反射可能是绕了一个长路。 - Matthew Scharley
想了一下(并心理上给了自己一个耳光),我认为这可能是做这件事情正确的唯一方法。 - Jeff Yates
无论如何,作为一个单一的条件语句,是的。 - Matthew Scharley
只有在转换正确时,使用try / catch才会更快。如果出现异常并在此处捕获它,则与反射相比,性能将受到严重影响而不是极小的影响。不要害怕反射,它仍然非常快。 - justin.m.chase

3

要确保的唯一方法是使用反射,但90%的情况下您可以通过使用array is T[]来避免该成本。大多数人将传递正确类型的数组,因此这样做就足够了。但是,您应该始终提供代码来执行反射检查,以防万一。以下是我的通用模板(注意:我是根据记忆在此编写的,因此可能无法编译,但应该能够给出基本思路):

class MyCollection : ICollection<T> {
   void ICollection<T>.CopyTo(T[] array, int index) {
       // Bounds checking, etc here.
       CopyToImpl(array, index);
   }
   void ICollection.CopyTo(Array array, int index) {
       // Bounds checking, etc here.
       if (array is T[]) { // quick, avoids reflection, but only works if array is typed as exactly T[]
           CopyToImpl((T[])localArray, index);
       } else {
           Type elementType = array.GetType().GetElementType();
           if (!elementType.IsAssignableFrom(typeof(T)) && !typeof(T).IsAssignableFrom(elementType)) {
               throw new Exception();
           }
           CopyToImpl((object[])array, index);
       }
   }
   private void CopyToImpl(object[] array, int index) {
       // array will always have a valid type by this point, and the bounds will be checked
       // Handle the copying here
   }
}

编辑:好的,我忘了指出一些东西。有几个答案天真地只使用了在这段代码中读作 element.IsAssignableFrom(typeof(T)) 的方法。你应该还允许 typeof(T).IsAssignableFrom(elementType),就像BCL一样,在这种情况下,如果开发人员知道这个特定的ICollection中所有的值实际上都是从T派生出的类型S,并传递了一个类型为S[]的数组。


你认为应该如何处理不是类型S的所有元素,以及抛出InvalidCastException的情况?用户需谨慎。 - Matthew Scharley
那就是BCL的方式。 - Alex Lyman
在C#中,“is”关键字是一个反射调用。所以如果绕路而行,你并没有节省多少时间。直接使用IsAssignableFrom更为简洁明了。 - justin.m.chase
@just in case: 实际上,在这种情况下,“is”是通过“isinst”CIL指令(ECMA-225第III部分,第4.6节)实现的。虽然从技术上讲,JIT允许调用反射来实现它,但MS-CLR和Mono都有非常高效的内联代码来实现它(每次取消装箱时也会运行)。 - Alex Lyman

1

List<T> 使用如下:

try
{
    Array.Copy(this._items, 0, array, index, this.Count);
}
catch (ArrayTypeMismatchException)
{
  //throw exception...
}

丑陋。我宁愿让它直接抛出异常。 - Jonathan Allen
List<T> 抛出 ArgumentException,这更加合适。 - Mark Cidade
既然你要付出努力,我建议你把 “ArrayTypeMismatch” 当做内部异常传递。 - Matthew Scharley
List<T> 已经有了 T ... 因此很容易确保你拥有正确的类型。 - justin.m.chase

0

这里是一个 try/catch 和反射的小测试:

object[] obj = new object[] { };
DateTime start = DateTime.Now;

for (int x = 0; x < 1000; x++)
{
    try
    {
        throw new Exception();
    }
    catch (Exception ex) { }
}
DateTime end = DateTime.Now;
Console.WriteLine("Try/Catch: " + (end - start).TotalSeconds.ToString());

start = DateTime.Now;

for (int x = 0; x < 1000; x++)
{
    bool assignable = typeof(int).IsAssignableFrom(obj.GetType().GetElementType());
}
end = DateTime.Now;
Console.WriteLine("IsAssignableFrom: " + (end - start).TotalSeconds.ToString());

在发布模式下的输出结果为:
Try/Catch: 1.7501001
IsAssignableFrom: 0

在调试模式下:

Try/Catch: 1.8171039
IsAssignableFrom: 0.0010001

结论,只需进行反思检查。这是值得的。

在计算性能时,你总是做出那个假设。 - justin.m.chase

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