在CLR中,强制转换和使用'as'关键字的区别

417

在编写接口时,我发现自己经常需要进行许多类型转换或对象类型转换。

这两种转换方法有什么区别吗?如果有的话,这是否会产生成本差异或者如何影响我的程序呢?

public interface IMyInterface
{
    void AMethod();
}

public class MyClass : IMyInterface
{
    public void AMethod()
    {
       //Do work
    }

    // Other helper methods....
}

public class Implementation
{
    IMyInterface _MyObj;
    MyClass _myCls1;
    MyClass _myCls2;

    public Implementation()
    {
        _MyObj = new MyClass();

        // What is the difference here:
        _myCls1 = (MyClass)_MyObj;
        _myCls2 = (_MyObj as MyClass);
    }
}

另外,"in general"通常使用哪种方法?


你能否在问题中添加一个小例子,说明为什么首先要使用强制转换,或者开始一个新的问题?我有点想知道为什么你只需要在单元测试中进行强制转换。不过我认为这超出了这个问题的范围。 - Erik van Brakel
2
我可以尝试更改我的单元测试以避免这种情况。基本上,问题在于我的具体对象中有一个属性,但该属性并不存在于接口中。我需要设置该属性,但在实际情况中,该属性已通过其他方式设置。这样回答您的问题吗? - Frank V
正如Patrik Hägne在下面敏锐地指出的那样,确实有区别。 - Neil
18个回答

548

以下回答是在2008年写的。

C# 7引入了模式匹配,这在很大程度上取代了as运算符,现在你可以这样写:

if (randomObject is TargetType tt)
{
    // Use tt here
}

请注意,此时tt仍在作用域内,但未明确赋值。(它在if语句的主体中被明确赋值。)在某些情况下,这可能会稍微有些恼人,因此如果您真的关心在每个作用域中引入最少数量的变量,您可能仍然希望使用is后跟一个转换操作。
我认为迄今为止(回答时!)没有任何答案真正解释了在哪种情况下值得使用哪种方法。
  • Don't do this:

    // Bad code - checks type twice for no reason
    if (randomObject is TargetType)
    {
        TargetType foo = (TargetType) randomObject;
        // Do something with foo
    }
    

    Not only is this checking twice, but it may be checking different things, if randomObject is a field rather than a local variable. It's possible for the "if" to pass but then the cast to fail, if another thread changes the value of randomObject between the two.

  • If randomObject really should be an instance of TargetType, i.e. if it's not, that means there's a bug, then casting is the right solution. That throws an exception immediately, which means that no more work is done under incorrect assumptions, and the exception correctly shows the type of bug.

    // 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;
    
  • If randomObject might be an instance of TargetType and TargetType is a reference type, then use code like this:

    TargetType convertedRandomObject = randomObject as TargetType;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject
    }
    
  • If randomObject might be an instance of TargetType and TargetType is a value type, then we can't use as with TargetType itself, but we can use a nullable type:

    TargetType? convertedRandomObject = randomObject as TargetType?;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject.Value
    }
    

    (Note: currently this is actually slower than is + cast. I think it's more elegant and consistent, but there we go.)

  • If you really don't need the converted value, but you just need to know whether it is an instance of TargetType, then the is operator is your friend. In this case it doesn't matter whether TargetType is a reference type or a value type.

  • There may be other cases involving generics where is is useful (because you may not know whether T is a reference type or not, so you can't use as) but they're relatively obscure.

  • I've almost certainly used is for the value type case before now, not having thought of using a nullable type and as together :)


注意,以上内容并未涉及性能问题,除了值类型的情况,我已经指出将值类型拆箱为可空值类型实际上更慢 - 但是一致。

根据naasking的答案,现代JIT在以下代码中显示is-and-cast或is-and-as与as-and-null-check一样快:

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加空检查肯定会更慢。上面的代码实际上很容易进行类型检查,因为它是针对一个密封类的; 如果你要检查一个接口,那么as加空检查的平衡会稍微倾向于这种情况。)
  • 它们都非常快。除非您真的不打算在之后对值进行任何操作,否则这根本不会成为您代码中的瓶颈。

所以我们不用担心性能问题。让我们关注正确性和一致性。

我坚持认为当处理变量时,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 所做的事情 - 显然是以一种更便宜的方式实现的。

10
以下是is/as/casting的IL成本,链接在此:http://www.atalasoft.com/cs/blogs/stevehawley/archive/2009/01/30/is-as-and-casting.aspx - plinth
3
如果目标对象可能是目标类型,为什么使用 "is" 和强制转换组合被认为是一种不好的实践?我的意思是,它会生成更慢的代码,但在这种情况下,意图比 AS 转换更清晰,例如:"如果 targetObject 是 targetType,则执行某些操作",而不是 "如果 targetObject 是 null,则执行某些操作"。此外,AS 子句将在 IF 范围之外创建一个不必要的变量。 - Valera Kolupaev
2
@Valera:很好的观点,尽管我建议as/null测试足够惯用,以至于几乎所有C#开发人员都能理解意图。个人不喜欢is + cast中涉及的重复。实际上,我希望有一种“仿佛”结构,可以同时执行两个操作。它们经常一起出现... - Jon Skeet
2
@Jon Skeet: 对不起我回复晚了。Is 和 Cast:2135,Is 和 As:2145,As 和 null 检查:1961,规格:操作系统:Windows Seven,CPU:i5-520M,4GB 的 DDR3 1033 RAM,在包含 128,000,000 个项的数组上进行基准测试。 - Behrooz
2
使用C# 7,您可以执行以下操作:if (randomObject is TargetType convertedRandomObject){ // Do stuff with convertedRandomObject.Value} 或者使用 switch/case 请参阅文档 - WerWet
显示剩余16条评论

79

"as"如果无法转换,则返回NULL。

在进行转换之前进行强制类型转换会引发异常。

为了性能考虑,引发异常通常需要更多的时间成本。


4
抛出异常的代价更高,但如果你知道对象可以正确转换,使用“as”需要更多时间,因为需要进行安全检查(请参见Anton的回答)。然而,我相信安全检查的代价非常小。 - user29439
17
考虑潜在引发异常的成本是一个因素,但通常这是正确的设计。 - Jeffrey L Whitledge
@panesofglass - 对于引用类型,无论是 as 还是 cast,转换兼容性都将始终在运行时进行检查,因此该因素不会区分两个选项。 (如果不是这样,那么 cast 就无法引发异常。) - Jeffrey L Whitledge
4
如果你需要使用一个先前生成的集合(例如非泛型集合),并且你的 API 中的某个方法需要一个 "Employees" 列表,但有人却传递了一个 "Products" 列表,那么抛出无效的转换异常可能是合适的,以表明接口要求的违规行为。 - Jeffrey L Whitledge
1
@user29439 抱歉,但是针对“as”运算符,“isinst” IL操作码比用于直接转换的“castclass”操作码更快。因此,对于引用类型,“as”即使对象可以无异常地转换,也会执行得更快。Unity引擎在使用IL2CPP时也会为“as”生成更高效的代码。 - dmitry1100
Unity引擎使用IL2CPP生成更高效的“as”代码。我也注意到了这一点,自从发现热路径上的任何内容都是如此,我已经养成了这样的习惯。 - WDUK

32

这里有另一个答案,附带一些IL的比较。考虑以下类:

public class MyClass
{
    public static void Main()
    {
        // Call the 2 methods
    }

    public void DirectCast(Object obj)
    {
        if ( obj is MyClass)
        { 
            MyClass myclass = (MyClass) obj; 
            Console.WriteLine(obj);
        } 
    } 


    public void UsesAs(object obj) 
    { 
        MyClass myclass = obj as MyClass; 
        if (myclass != null) 
        { 
            Console.WriteLine(obj);
        } 
    }
}

现在看一下每个方法生成的IL代码。即使操作码对你来说毫无意义,你也可以看到一个主要区别——DirectCast方法调用了isinst,然后是castclass。所以基本上是两个调用而不是一个。

.method public hidebysig instance void  DirectCast(object obj) cil managed
{
  // Code size       22 (0x16)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  isinst     MyClass
  IL_0006:  brfalse.s  IL_0015
  IL_0008:  ldarg.1
  IL_0009:  castclass  MyClass
  IL_000e:  pop
  IL_000f:  ldarg.1
  IL_0010:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0015:  ret
} // end of method MyClass::DirectCast

.method public hidebysig instance void  UsesAs(object obj) cil managed
{
  // Code size       17 (0x11)
  .maxstack  1
  .locals init (class MyClass V_0)
  IL_0000:  ldarg.1
  IL_0001:  isinst     MyClass
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  brfalse.s  IL_0010
  IL_000a:  ldarg.1
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ret
} // end of method MyClass::UsesAs

isinst关键字与castclass的区别

这篇博客文章对两种方式进行了比较。他的总结如下:

  • 在直接比较中,isinst比castclass快(尽管只是稍微快一点)
  • 当需要执行检查以确保转换成功时,isinst比castclass快得多
  • 不应该同时使用isinst和castclass,因为这比最快的“安全”转换慢得多(慢了超过12%)

我个人总是使用As,因为它易于阅读,并由.NET开发团队(或者至少是Jeffrey Richter)推荐。


我一直在寻找关于强制转换和as的清晰解释,这个答案通过逐步解释常见的中间语言步骤使其更加清晰易懂。谢谢! - Morse

19

两者之间更微妙的区别之一是当涉及到强制类型转换操作符时,“as”关键字不能用于转换:

public class Foo
{
    public string Value;

    public static explicit operator string(Foo f)
    {
        return f.Value;
    }

}

public class Example
{
    public void Convert()
    {
        var f = new Foo();
        f.Value = "abc";

        string cast = (string)f;
        string tryCast = f as string;
    }
}

尽管我认为在以前的版本中可以编译,但是最后一行不会编译,因为"as"关键字不考虑类型转换运算符。不过,string cast = (string)f;这行代码可以正常工作。


13

as只对引用类型起作用,如果无法进行转换,则不会抛出异常,而是返回null。因此,使用as与以下代码基本等效:

_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;

另一方面,C风格的强制类型转换在无法转换时会抛出异常。

4
等价的,是的,但并不相同。这会生成比使用"as"更多的代码。 - plinth

10

这并不是对你问题的回答,而是我认为的一个重要相关点。

如果你按照接口编程,就不应该需要进行强制类型转换。希望这种情况很少出现。如果不是这样,那么你可能需要重新考虑一下你的接口设计。


到目前为止,强制类型转换主要是用于我的单元测试,但感谢您提出这个问题。在我继续工作时,我会记住这一点的。 - Frank V
我同意toad的观点,@Frank V,我也很好奇单元测试方面对于你来说为什么如此重要。如果需要进行强制类型转换,通常意味着需要重新设计或重构,因为这表明您正在试图将不同的问题塞进应该以不同方式管理的地方。 - The Senator
@TheSenator 这个问题已经超过3年了,所以我真的记不清了。但是我可能在单元测试时甚至会积极使用接口。可能是因为我在使用工厂模式并且无法访问目标对象上的公共构造函数进行测试。 - Frank V

8
请忽略Jon Skeet的建议,即避免测试和转换模式,例如:
if (randomObject is TargetType)
{
    TargetType foo = randomObject as TargetType;
    // Do something with foo
}

这个想法比铸造和零测试成本更高是一个谬论
TargetType convertedRandomObject = randomObject as TargetType;
if (convertedRandomObject != null)
{
    // Do stuff with convertedRandomObject
}

这是一种微小的优化,但实际上并不起作用。我运行了一些真实的测试,测试和转换比转换和空比较更快,而且更安全,因为如果转换失败,你在if外部的范围内就不可能有null引用。
如果你想知道为什么测试和转换更快,或者至少不会更慢,有一个简单和复杂的原因。
简单原因:即使是朴素的编译器也会将两个类似的操作(如测试和转换)合并成一个单独的测试和分支。转换和null测试可能会强制进行两次测试和分支,一次用于类型测试和转换失败时的null转换,一次用于null检查本身。最起码,它们都会被优化为单个测试和分支,因此测试和转换既不比转换和null测试更慢,也不比它更快。 复杂:为什么测试和转换更快:转换和空测试会在外部范围引入另一个变量,编译器必须跟踪其生存状态,并且根据控制流的复杂程度可能无法优化该变量。相反,测试和转换仅在限定范围内引入新变量,因此编译器知道该变量在范围退出后即死亡,因此可以更好地优化寄存器分配。

因此,请让“转换和空测试比测试和转换更好”的建议消失。请。测试和转换既更安全又更快。


8
如果您进行两次测试(根据您的第一个代码片段),如果它是字段或“ref”参数,则有可能在两次测试之间更改类型。对于局部变量来说是安全的,但对于字段不是这样。我很想运行您的基准测试,但您在博客文章中给出的代码并不完整。我同意不进行微观优化,但我认为使用两次值并不比使用“as”和空性测试更可读或优雅。(顺便说一句,在is之后,我肯定会使用直接强制转换而不是“as”) - Jon Skeet
5
我不明白为什么这样做更安全。实际上,我已经证明了它更加不安全。当然,你会在作用域内得到一个可能为空的变量,但是除非你在接下来的“if”块之外开始使用它,否则一切都没问题。我提出的安全问题(涉及字段更改其值)是针对所示代码的真正担忧——你的安全问题需要开发人员在其他代码中变得放松。 - Jon Skeet
1
+1,指出 is/cast 或 as/cast 在现实中并没有变慢。我自己运行了完整的测试,可以确认就我而言并没有任何区别 - 而且坦白说,你可以在非常短的时间内运行 惊人的 大量转换。将更新我的答案,并附上完整的代码。 - Jon Skeet
1
确实,如果绑定不是本地的话,就有可能出现TOCTTOU漏洞(检查时间与使用时间),所以这是一个很好的观点。至于为什么更安全,我与许多喜欢重复使用本地变量的初级开发人员一起工作。在我的经验中,cast-and-null是一个非常真实的危险,而且我从来没有遇到过TOCTTOU的情况,因为我不是用这种方式设计我的代码。至于运行时测试速度,它甚至比虚拟分派[1]还要快!关于代码,我会看看能否找到cast测试的源代码。[1] http://higherlogics.blogspot.com/2008/10/vtable-dispatching-vs-runtime-tests-and.html - naasking
1
@naasking:我从来没有遇到本地重用问题 - 但我认为在代码审查中比更微妙的TOCTTOU错误更容易发现。另外值得指出的是,我刚刚重新运行了我的基准测试,检查接口而不是封闭类,并且这有利于使用as-then-null-check的性能...但正如我所说,性能不是我选择任何特定方法的原因。 - Jon Skeet
显示剩余2条评论

4
如果转换失败,'as' 关键字不会抛出异常,而是将变量设置为 null(或对于值类型,将其设置为默认值)。

3
值类型没有默认值。无法使用 "as" 运算符进行值类型转换。 - Patrik Hägne
2
“as”关键字实际上不适用于值类型,因此它总是设置为null。 - Erik van Brakel

4

这不是对问题的回答,而是对问题代码示例的评论:

通常情况下,您不需要将一个对象从例如IMyInterface转换为MyClass。接口的好处在于,如果您将一个实现接口的对象作为输入,则无需关心您正在获取哪种类型的对象。

如果您将IMyInterface强制转换为MyClass,则已经假设您获取了一个MyClass类型的对象,并且使用IMyInterface没有任何意义,因为如果您使用实现IMyInterface的其他类来提供代码,则会破坏您的代码...

现在,我的建议是:如果您的接口设计得很好,您可以避免很多类型转换。


3
as操作符只能用于引用类型,不能被重载,如果操作失败,它将返回null,但不会抛出异常。
强制转换可以用于任何兼容的类型,可以被重载,如果操作失败,它将抛出异常。
使用哪种方式取决于具体情况。主要是看你是否想在转换失败时抛出异常。

1
'as' 也可以用于可空值类型,这提供了一个有趣的模式。请参见我的答案代码。 - Jon Skeet

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