在编程接口时,我发现我做了很多强制转换或对象类型转换。

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

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);
    }
}

另外,“一般来说”首选的方法是什么?


当前回答

下面的答案写于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 :)


编辑:请注意,上面没有讨论性能,除了值类型的情况,其中我已经注意到,解盒到可空值类型实际上更慢——但一致。

根据naask的回答,is-and-cast或is-and-as都和现代jit中的As -and-null检查一样快,如下所示:

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检查确实更慢。上面的代码实际上使类型检查变得很容易,因为它是针对密封类的;如果你正在检查一个接口,平衡会略微倾向于as-plus-null检查。) 它们都快得不可思议。这不会成为您代码中的瓶颈,除非您以后真的不打算对这些值做任何事情。

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

我认为is-and-cast(或is-and-as)在处理变量时都是不安全的,因为它所引用的值的类型可能会因为测试和强制转换之间的另一个线程而改变。这将是一个相当罕见的情况-但我宁愿有一个惯例,我可以一直使用。

我还认为,“当时为空”检查提供了更好的关注点分离。一个语句尝试进行转换,另一个语句使用转换结果。is-and-cast或is-and-as执行一个测试,然后再次尝试转换值。

换句话说,有人会这样写吗:

int value;
if (int.TryParse(text, out value))
{
    value = int.Parse(text);
    // Use value
}

这就是is-and-cast正在做的事情——尽管显然是以一种相当便宜的方式。

其他回答

这取决于,你想检查null后使用"as"或你更喜欢你的应用程序抛出一个异常?

我的经验法则是,如果我总是期望变量在我想要的时候是我期望的类型,我就使用强制转换。如果变量可能不会强制转换为我想要的,并且我准备从使用as处理空值,那么我将使用as。

这是另一个答案,带有一些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%以上)

我个人总是使用a,因为它易于阅读,并且是。net开发团队(或者Jeffrey Richter)推荐的。

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

如果你正在为一个接口编程,你不应该需要强制转换。希望这种类型的演员很少。如果不是,您可能需要重新考虑一些接口。

as操作符只能用于引用类型,不能重载,如果操作失败,它将返回null。它永远不会抛出异常。

强制转换可以用于任何兼容的类型,它可以重载,如果操作失败,它将抛出异常。

使用哪一种取决于具体情况。首先,这是一个是否要对失败的转换抛出异常的问题。

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

通常你不需要强制转换一个对象,例如IMyInterface到MyClass。接口的伟大之处在于,如果你把一个对象作为实现接口的输入,那么你就不需要关心你得到的是什么类型的对象。

如果你将IMyInterface转换为MyClass,那么你已经假设你得到了一个MyClass类型的对象,使用IMyInterface是没有意义的,因为如果你用其他实现IMyInterface的类来填充你的代码,它会破坏你的代码……

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