可疑的ArgumentException

Posted by eagleboost on October 13, 2024

  项目做得久了,千奇百怪的问题都会遇到。大多数问题一看就知道怎么回事,有些问题则很有迷惑性,甚至让人百思不得其解。

  最近用户报告了一个错误,分析日志后发现往一个ObservableCollection<T>里面插入数据的时候抛出了System.ArgumentException,类似下面这样。看起来是内部出现了InvalidCast,问题是这里的xxx确实是yyy类型,不应该出现无效类型转换的异常。

1
System.ArgumentException: The value "xxx" is not of type "yyy" and cannot be used in this generic collection. (Parameter 'value')

  如果xxxyyy类型真的不兼容,那么用下面的代码很容易重现这个异常:

1
2
3
4
5
6
7
8
9
10
public interface IContract
{
}

public class Implementation : IContract
{
}

IList list = new ObservableCollection<IContract>(); //使用非泛型接口来调用
list.Add(new object()); //抛出System.ArgumentException

  ObservableCollection<T>的非泛型Add方法来自Collection<T>,看起来完全没问题,那么问题出在哪里呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int IList.Add(object value)
{
  if (this.items.IsReadOnly)
    ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection);
  ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(value, ExceptionArgument.value);
  try
  {
    this.Add((T) value);
  }
  catch (InvalidCastException ex)
  {
    ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof (T));
  }
  return this.Count - 1;
}

  好在生产环境出现的这个问题很容易重现,调试后发现有点意思。我们知道ObservableCollection<T>实现了INotifyCollectionChanged接口,在增删改数据的时候会触发事件,所以如果代码事件处理代码恰好抛出了InvalidCastException被上面的IList.Add方法捕捉到就会抛出令人迷惑的ArgumentException,比如下面的代码:

1
2
3
4
var coll = new ObservableCollection<IContract>();
coll.CollectionChanged += (s, e) => throw new InvalidCastException();
IList list = coll;
list.Add(new Implementation());

  Collection<T>的这段IList.Add方法与List<T>的实现几乎完全一样。对List<T>来说没问题,但Collection<T>中存在可重载的方法,所以代码需要改进一下:即先进行类型转换,再调用this.Add。我会把代码这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int IList.Add(object value)
{
  ...
  this.Add(CastValue());
  
  return this.Count - 1;

  T CastValue()
  {
    try
    {
      return (T) value;
    }
    catch (InvalidCastException ex)
    {
      ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof (T));
    }
  }
}

  这个问题只在.NetFramework中出现,.Net 6.0以上版本已经有了新的实现 ,与上面的代码类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int IList.Add(object? value)
{
    if (items.IsReadOnly)
    {
        ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection);
    }
    ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(value, ExceptionArgument.value);

    T? item = default;

    try
    {
        item = (T)value!;
    }
    catch (InvalidCastException)
    {
        ThrowHelper.ThrowWrongValueTypeArgumentException(value, typeof(T));
    }

    Add(item);

    return this.Count - 1;
}