关于c#:使用CLR中的’as’关键字进行转换

Casting vs using the 'as' keyword in the CLR

在编程接口时,我发现我正在进行大量的转换或对象类型转换。

这两种转换方法有区别吗?如果是这样,是否存在成本差异,或者这对我的计划有何影响?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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操作符,正如您现在可以写的:

1
2
3
4
if (randomObject is TargetType tt)
{
    // Use tt here
}

请注意,在此之后,tt仍在范围内,但未明确分配。(它肯定是在if体中指定的)这在某些情况下有点烦人,因此如果您真的关心在每个范围中引入尽可能少的变量数量,您可能仍然希望使用is后面跟一个强制转换。

到目前为止,我认为没有任何答案(在开始这个答案的时候!)真的解释了哪些地方值得使用。

  • 不要这样做:

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

    如果randomObject是字段而不是局部变量,那么这种检查不仅是两次,而且可能是检查不同的事情。如果"if"可以通过,但如果另一个线程在两个线程之间更改EDOCX1的值(4),则强制转换将失败。

  • 如果randomObject真的应该是TargetType的一个实例,也就是说,如果不是,那就意味着存在一个bug,那么casting就是正确的解决方案。这会立即抛出异常,这意味着在不正确的假设下不会再做任何工作,并且异常会正确地显示错误的类型。

    1
    2
    3
    4
    // 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是引用类型,那么使用如下代码:

    1
    2
    3
    4
    5
    TargetType convertedRandomObject = randomObject as TargetType;
    if (convertedRandomObject != null)
    {
        // Do stuff with convertedRandomObject
    }
  • 如果randomObject可能是TargetType的一个实例,TargetType是一个值类型,那么我们不能将asTargetType本身一起使用,但是我们可以使用一个可以为空的类型:

    1
    2
    3
    4
    5
    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和CAST或IS和AS都与现代JIT一样快,都是空检查,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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加零检查肯定较慢。上面的代码实际上使类型检查变得容易,因为它是针对一个密封类的;如果您要检查一个接口,平衡提示稍微偏向于a s加上NULL检查。)
  • 他们都快得要命。这不会成为代码中的瓶颈,除非您以后真的不打算对值做任何事情。

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

我认为在处理变量时,is和cast(或is和as)都是不安全的,因为它引用的值的类型可能会由于测试和cast之间的另一个线程而改变。这将是一个非常罕见的情况-但我宁愿有一个公约,我可以一直使用。

我还认为as-then空检查可以更好地分离关注点。我们有一个语句尝试转换,然后有一个语句使用结果。IS和CAST或IS和AS执行测试,然后再次尝试转换值。

换一种说法,有人会写:

1
2
3
4
5
6
int value;
if (int.TryParse(text, out value))
{
    value = int.Parse(text);
    // Use value
}

这就是现在和演员们正在做的事情——尽管显然是以一种更便宜的方式。


如果无法强制转换,"as"将返回空值。

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

对于性能,引发异常通常在时间上花费更大的成本。


这是另一个答案,通过一些IL比较。考虑课程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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。即使这些操作代码对您没有任何意义,您也可以看到一个主要的区别:ISISist被调用,然后在DirectCast方法中被castclass调用。所以基本上是两个电话而不是一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.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

isist关键字与castclass

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

  • 在直接比较中,isist比castclass快(尽管只是稍微快一点)
  • 当必须执行检查以确保转换成功时,isist比castclass快得多。
  • 不应使用isist和castclass的组合,因为这比最快的"安全"转换慢得多(慢12%以上)。

我个人总是使用as,因为它很容易阅读,并且是.NET开发团队(或Jeffrey Richter)推荐的。


两者之间更细微的区别之一是,当涉及到强制转换运算符时,"as"关键字不能用于强制转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"关键字不考虑cast操作符。不过,string cast = (string)f;线的运行情况很好。


如果它不能执行返回空值的转换,则从不引发异常(仅对引用类型进行操作)。所以使用as基本上等同于

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

另一方面,C样式的强制转换在无法转换时抛出异常。


不是你问题的真正答案,但我认为是一个重要的相关点。

如果您正在编程到一个接口,则不需要强制转换。希望这些石膏非常罕见。如果不是,您可能需要重新考虑一些接口。


请忽略乔恩·斯基特的建议,回复:避免测试和投射模式,即:

1
2
3
4
5
if (randomObject is TargetType)
{
    TargetType foo = randomObject as TargetType;
    // Do something with foo
}

这比一个演员表和一个空测试花费更多的想法是一个神话:

1
2
3
4
5
TargetType convertedRandomObject = randomObject as TargetType;
if (convertedRandomObject != null)
{
    // Do stuff with convertedRandomObject
}

这是一个不起作用的微观优化。我运行了一些实际的测试,并且test和cast实际上比cast和NULL比较快,而且它也更安全,因为如果强制转换失败,那么在if之外的范围内就不可能有空引用。

如果您想知道为什么测试和强制转换更快,或者至少不慢,有一个简单而复杂的原因。

简单:即使是幼稚的编译器也会将两个类似的操作(如test和cast)合并到一个测试和分支中。强制转换和空测试可以强制两个测试和一个分支,一个用于类型测试,一个用于失败时转换为空,一个用于空检查本身。至少,它们都将优化为单个测试和分支,因此测试和强制转换既不慢也不快于强制转换和空测试。

复杂:为什么测试和强制转换更快:强制转换和空测试将另一个变量引入外部作用域,编译器必须跟踪该外部作用域的活动性,并且它可能无法根据控制流的复杂程度来优化该变量。相反,test和cast只在限定的范围内引入一个新的变量,这样编译器就知道该变量在范围退出后就死了,因此可以更好地优化寄存器分配。

所以,请让这个"空测试比测试更好"的建议模具。拜托。测试和铸造既安全又快速。


如果强制转换失败,"as"关键字不会引发异常;而是将变量设置为空(或值类型的默认值)。


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

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

如果您将imyinterface强制转换为myClass,而不是假定您已经得到了一个myClass类型的对象,那么使用imyinterface是没有意义的,因为如果您将代码与实现imyinterface的其他类一起提供,它将破坏您的代码…

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


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

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

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


我的答案只是关于速度,在不检查类型和不检查空值的情况下。我在jon skeet的代码中添加了两个附加测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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);
    }
}

结果:

1
2
3
4
5
Is and Cast: 30000000 : 88
Is and As: 30000000 : 93
As and null check: 30000000 : 56
Cast: 30000000 : 66
As: 30000000 : 46

不要像我一样把注意力集中在速度上,因为这一切都很快。


除了这里已经曝光的所有内容之外,我刚刚发现了一个我认为值得注意的实际差异,即明确的演员阵容

1
var x = (T) ...

而不是使用as操作符。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(GenericCaster<string>(12345));
        Console.WriteLine(GenericCaster<object>(new { a = 100, b ="string" }) ??"null");
        Console.WriteLine(GenericCaster<double>(20.4));

        //prints:
        //12345
        //null
        //20.4

        Console.WriteLine(GenericCaster2<string>(12345));
        Console.WriteLine(GenericCaster2<object>(new { a = 100, b ="string" }) ??"null");

        //will not compile -> 20.4 does not comply due to the type constraint"T : class"
        //Console.WriteLine(GenericCaster2<double>(20.4));
    }

    static T GenericCaster<T>(object value, T defaultValue = default(T))
    {
        T castedValue;
        try
        {
            castedValue = (T) Convert.ChangeType(value, typeof(T));
        }
        catch (Exception)
        {
            castedValue = defaultValue;
        }

        return castedValue;
    }

    static T GenericCaster2<T>(object value, T defaultValue = default(T)) where T : class
    {
        T castedValue;
        try
        {
            castedValue = Convert.ChangeType(value, typeof(T)) as T;
        }
        catch (Exception)
        {
            castedValue = defaultValue;
        }

        return castedValue;
    }
}

底线:GenericCaster2不能用于结构类型。普通施法者会。


如果使用面向.NET Framework 4.x的Office PIA,则应使用as关键字,否则将无法编译。

1
2
Microsoft.Office.Interop.Outlook.Application o = new Microsoft.Office.Interop.Outlook.Application();
Microsoft.Office.Interop.Outlook.MailItem m = o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem) as Microsoft.Office.Interop.Outlook.MailItem;

当以.NET 2.0为目标时,强制转换是可以的,但是:

1
Microsoft.Office.Interop.Outlook.MailItem m = (Microsoft.Office.Interop.Outlook.MailItem)o.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olMailItem);

以.NET 4.x为目标时,错误为:

错误CS0656:缺少编译器所需的成员"Microsoft.CSharp.RuntimeBinder.Binder.Convert"

错误CS0656:缺少所需的编译器成员"microsoft.csharp.runtimebinder.csharpargumentinfo.create"


看看这些链接:

  • http://gen5.info/q/2008/06/13/prefix-casting-versus-as-casting-in-c/
  • http://www.codeproject.com/articles/8052/type-casting-impact-over-execution-performance-in

它们向您展示了一些细节和性能测试。


你的选择很大程度上取决于你需要什么。我更喜欢直白的演员阵容

1
IMyInterface = (IMyInterface)someobj;

因为如果对象应该是imyinterface类型,而不是-这绝对是个问题。最好尽早得到误差,因为准确的误差将被修正而不是修正其副作用。

但是,如果您处理的方法接受object作为参数,那么您需要在执行任何代码之前检查它的确切类型。在这种情况下,as是有用的,这样你就可以避免InvalidCastException


OP的问题仅限于特定的铸造情况。这个标题涵盖了更多的情况。以下是我目前能想到的所有相关的演员阵容情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
private class CBase
{
}

private class CInherited : CBase
{
}

private enum EnumTest
{
  zero,
  one,
  two
}

private static void Main (string[] args)
{
  //########## classes ##########
  // object creation, implicit cast to object
  object oBase = new CBase ();
  object oInherited = new CInherited ();

  CBase oBase2 = null;
  CInherited oInherited2 = null;
  bool bCanCast = false;

  // explicit cast using"()"
  oBase2 = (CBase)oBase;    // works
  oBase2 = (CBase)oInherited;    // works
  //oInherited2 = (CInherited)oBase;   System.InvalidCastException
  oInherited2 = (CInherited)oInherited;    // works

  // explicit cast using"as"
  oBase2 = oBase as CBase;
  oBase2 = oInherited as CBase;
  oInherited2 = oBase as CInherited;  // returns null, equals C++/CLI"dynamic_cast"
  oInherited2 = oInherited as CInherited;

  // testing with Type.IsAssignableFrom(), results (of course) equal the results of the cast operations
  bCanCast = typeof (CBase).IsAssignableFrom (oBase.GetType ());    // true
  bCanCast = typeof (CBase).IsAssignableFrom (oInherited.GetType ());    // true
  bCanCast = typeof (CInherited).IsAssignableFrom (oBase.GetType ());    // false
  bCanCast = typeof (CInherited).IsAssignableFrom (oInherited.GetType ());    // true

  //########## value types ##########
  int iValue = 2;
  double dValue = 1.1;
  EnumTest enValue = EnumTest.two;

  // implicit cast, explicit cast using"()"
  int iValue2 = iValue;   // no cast
  double dValue2 = iValue;  // implicit conversion
  EnumTest enValue2 = (EnumTest)iValue;  // conversion by explicit cast. underlying type of EnumTest is int, but explicit cast needed (error CS0266: Cannot implicitly convert type 'int' to 'test01.Program.EnumTest')

  iValue2 = (int)dValue;   // conversion by explicit cast. implicit cast not possible (error CS0266: Cannot implicitly convert type 'double' to 'int')
  dValue2 = dValue;
  enValue2 = (EnumTest)dValue;  // underlying type is int, so"1.1" beomces"1" and then"one"

  iValue2 = (int)enValue;
  dValue2 = (double)enValue;
  enValue2 = enValue;   // no cast

  // explicit cast using"as"
  // iValue2 = iValue as int;   error CS0077: The as operator must be used with a reference type or nullable type
}


这取决于使用"as"后是否要检查空值,还是希望应用程序引发异常?

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


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