行下的答案写于 2008 年。
C# 7 引入了模式匹配,它在很大程度上取代了as
运算符,您现在可以编写:
if (randomObject is TargetType tt)
{
// Use tt here
}
请注意,tt
此后仍在范围内,但未明确分配。(它肯定是在主体内分配的if
。)在某些情况下这有点烦人,所以如果你真的关心在每个范围内引入尽可能少的变量,你可能仍然希望使用is
后跟强制转换。
到目前为止,我认为没有任何答案(在开始此答案时!)真正解释了值得在哪里使用哪个。
不要这样做:
// Bad code - checks type twice for no reason
if (randomObject is TargetType)
{
TargetType foo = (TargetType) randomObject;
// Do something with foo
}
这不仅检查了两次,而且可能检查不同的东西,如果randomObject
是字段而不是局部变量。如果另一个线程更改了randomObject
两者之间的值,则“if”可能会通过但强制转换失败。
如果randomObject
真的应该是 的一个实例TargetType
,即如果不是,则意味着存在错误,那么强制转换是正确的解决方案。这会立即引发异常,这意味着在不正确的假设下不再进行任何工作,并且异常正确地显示了错误的类型。
// This will throw an exception if randomObject is non-null and
// refers to an object of an incompatible type. The cast is
// the best code if that's the behaviour you want.
TargetType convertedRandomObject = (TargetType) randomObject;
如果randomObject
可能TargetType
是并且TargetType
是引用类型的实例,则使用如下代码:
TargetType convertedRandomObject = randomObject as TargetType;
if (convertedRandomObject != null)
{
// Do stuff with convertedRandomObject
}
如果randomObject
可能TargetType
是并且是值类型的实例TargetType
,那么我们不能as
与TargetType
自身一起使用,但我们可以使用可为空的类型:
TargetType? convertedRandomObject = randomObject as TargetType?;
if (convertedRandomObject != null)
{
// Do stuff with convertedRandomObject.Value
}
(注意:目前这实际上比 is + cast 慢。我认为它更优雅和一致,但我们开始了。)
如果你真的不需要转换后的值,但你只需要知道它是否是TargetType 的实例,那么is
操作符就是你的朋友。在这种情况下,TargetType 是引用类型还是值类型都没有关系。
可能还有其他涉及泛型is
的情况有用(因为您可能不知道 T 是否是引用类型,因此您不能使用 as),但它们相对模糊。
我几乎可以肯定is
在此之前使用过值类型的情况,没有考虑过使用可空类型并as
一起使用:)
编辑:请注意,除了值类型案例之外,以上都没有谈到性能,我注意到拆箱到可空值类型实际上更慢 - 但一致。
根据 naasking 的回答,is-and-cast 或 is-and-as 都与现代 JIT 的空值检查一样快,如下面的代码所示:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i + 1] = "x";
values[i + 2] = new object();
}
FindLengthWithIsAndCast(values);
FindLengthWithIsAndAs(values);
FindLengthWithAsAndNullCheck(values);
}
static void FindLengthWithIsAndCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int len = 0;
foreach (object o in values)
{
if (o is string)
{
string a = (string) o;
len += a.Length;
}
}
sw.Stop();
Console.WriteLine("Is and Cast: {0} : {1}", len,
(long)sw.ElapsedMilliseconds);
}
static void FindLengthWithIsAndAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int len = 0;
foreach (object o in values)
{
if (o is string)
{
string a = o as string;
len += a.Length;
}
}
sw.Stop();
Console.WriteLine("Is and As: {0} : {1}", len,
(long)sw.ElapsedMilliseconds);
}
static void FindLengthWithAsAndNullCheck(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int len = 0;
foreach (object o in values)
{
string a = o as string;
if (a != null)
{
len += a.Length;
}
}
sw.Stop();
Console.WriteLine("As and null check: {0} : {1}", len,
(long)sw.ElapsedMilliseconds);
}
}
在我的笔记本电脑上,这些都在大约 60 毫秒内执行。有两点需要注意:
- 它们之间没有显着差异。(事实上,在某些情况下,as-plus-null-check 肯定会更慢。上面的代码实际上使类型检查变得容易,因为它是针对密封类的;如果您正在检查接口,则余额会稍微提示支持 as-plus-null-check。)
- 他们都快得离谱。这根本不会成为您代码中的瓶颈,除非您之后真的不打算对这些值做任何事情。
所以我们不用担心性能。让我们担心正确性和一致性。
我坚持认为 is-and-cast(或 is-and-as)在处理变量时都是不安全的,因为它所引用的值的类型可能会由于测试和强制转换之间的另一个线程而改变。那将是一种非常罕见的情况-但我宁愿有一个可以始终使用的约定。
我还认为 as-then-null-check 可以更好地分离关注点。我们有一个尝试转换的语句,然后有一个使用结果的语句。is-and-cast 或 is-and-as 执行测试,然后再次尝试转换值。
换句话说,有人会写:
int value;
if (int.TryParse(text, out value))
{
value = int.Parse(text);
// Use value
}
这就是 is-and-cast 正在做的事情——尽管显然是以一种相当便宜的方式。