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

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

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正在做的事情——尽管显然是以一种相当便宜的方式。

其他回答

如果不能强制转换,"as"将返回NULL。

之前的强制转换将引发异常。

对于性能而言,引发异常的代价通常更大。

我的答案只是在我们不检查类型和转换后不检查null的情况下的速度。我在Jon Skeet的代码中添加了两个额外的测试:

using System;
using System.Diagnostics;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];

        for (int i = 0; i < Size; i++)
        {
            values[i] = "x";
        }
        FindLengthWithIsAndCast(values);
        FindLengthWithIsAndAs(values);
        FindLengthWithAsAndNullCheck(values);

        FindLengthWithCast(values);
        FindLengthWithAs(values);

        Console.ReadLine();
    }

    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);
    }
    static void FindLengthWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = (string)o;
            len += a.Length;
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindLengthWithAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int len = 0;
        foreach (object o in values)
        {
            string a = o as string;
            len += a.Length;
        }
        sw.Stop();
        Console.WriteLine("As: {0} : {1}", len,
                          (long)sw.ElapsedMilliseconds);
    }
}

结果:

Is and Cast: 30000000 : 88
Is and As: 30000000 : 93
As and null check: 30000000 : 56
Cast: 30000000 : 66
As: 30000000 : 46

不要像我一样专注于速度,因为所有这些都是非常非常快的。

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

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

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

如果转换失败,'as'关键字不会抛出异常;它将变量设置为null(或为值类型的默认值)。

as关键字的工作原理与兼容引用类型之间的显式强制转换相同,主要区别在于转换失败时它不会引发异常。相反,它在目标变量中产生一个空值。由于异常在性能方面非常昂贵,因此它被认为是一种更好的强制转换方法。