项目做得久了,千奇百怪的问题都会遇到。大多数问题一看就知道怎么回事,有些问题则很有迷惑性,甚至让人百思不得其解。
最近用户报告了一个错误,分析日志后发现往一个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')
如果xxx
与yyy
类型真的不兼容,那么用下面的代码很容易重现这个异常:
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;
}