使用CLR中的'as'关键字进行转换

时间:2009-01-30 16:20:53

标签: c# casting clr

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

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

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

此外,什么是“一般”首选方法?

18 个答案:

答案 0 :(得分:493)

该行的答案写于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是值类型,那么我们就无法使用{{1} } as本身,但我们可以使用可空类型:

    TargetType

    (注意:目前这是actually slower than is + cast。我认为它更优雅和一致,但我们去了。)

  • 如果您确实不需要转换后的值,但只需知道是TargetType的实例,那么TargetType? convertedRandomObject = randomObject as TargetType?; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject.Value } 运算符就是您的朋友。在这种情况下,TargetType是引用类型还是值类型无关紧要。

  • 可能还有其他涉及泛型的案例is很有用(因为你可能不知道T是否是引用类型,因此你不能使用as)但是它们相对模糊不清

  • 我之前几乎肯定使用is作为值类型的情况,没有考虑过使用可空类型和is:)


编辑:请注意,除了值类型的情况之外,以上都没有讨论性能,我注意到,取消装入可以为空的值类型实际上更慢 - 但是一致。

根据naasking的回答,is-and-cast或is-and-as与现代JIT一样快和as-and-check一样快,如下面的代码所示:

as

在我的笔记本电脑上,这些都在大约60ms内执行。有两点需要注意:

  • 它们之间没有显着差异。 (实际上,在某些情况下,as-plus-null-check肯定 更慢。上面的代码实际上使类型检查变得容易,因为它是一个密封的类;如果你正在检查一个接口,余额略微倾向于as-plus-null-check。)
  • 他们所有疯狂快。这只是不会成为你代码中的瓶颈,除非你真的不打算用之后的值做任何

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

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

我还认为as-then-null-check可以更好地分离关注点。我们有一个语句尝试转换,然后一个语句使用结果。 is-and-cast或is-and-as执行测试,然后再次尝试转换该值。

换句话说,任何人写道:

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

这就是现在正在做的事情 - 虽然显然是以相当便宜的方式。

答案 1 :(得分:67)

“as”如果无法投射,将返回NULL。

在之前

投射会引发异常。

对于表现,提出例外通常会在时间上花费更多。

答案 2 :(得分:24)

这是另一个答案,有一些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关键字与投射类

This blog post在两种方式之间进行了比较。他的总结是:

  • 在直接比较中,isinst比castclass更快(虽然只是略微)
  • 当必须执行检查以确保转换成功时,isinst明显快于castclass
  • 不应使用isinst和castclass的组合,因为这比最快的“安全”转换要慢得多(速度超过12%)

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

答案 3 :(得分:18)

两者之间的一个更微妙的区别是,当涉及演员操作员时,“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;行虽然运行得很好。

答案 4 :(得分:12)

as 如果无法执行转换,则不会抛出异常 null 而<(> em 仅对引用类型进行操作)。所以使用作为基本上等同于

_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;
另一方面,如果无法进行转换,则C样式转换会抛出异常。

答案 5 :(得分:10)

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

如果您正在编程接口,则不需要进行转换。希望这些演员阵容非常罕见。如果不是,您可能需要重新考虑一些接口。

答案 6 :(得分:9)

请忽略Jon Skeet的建议,重新:避免测试和转换模式,即:

if (randomObject is TargetType)
{
    TargetType foo = randomObject as TargetType;
    // Do something with foo
}

这个费用超过演员和空测试的想法是 MYTH

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

这是微观优化,不起作用。我运行some real tests,测试和转换实际上比cast-and-null-comparison更快,而且它也更安全,因为你不可能在if之外的范围内有一个空引用演员失败了。

如果你想要一个测试和转换速度更快,或者至少不慢的原因,那就是一个简单而复杂的原因。

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

复杂:为什么test-and cast更快:cast-and-null-test将另一个变量引入外部作用域,编译器必须跟踪它的活跃度,并且它可能无法优化远离变量取决于控制流的复杂程度。相反,测试和转换仅在分隔范围内引入了一个新变量,因此编译器知道在范围退出后该变量已经死亡,因此可以更好地优化寄存器分配。

所以请,请让这个“cast-and-null-test比测试和演员更好”的建议DIE。请。测试和铸造既安全又快捷。

答案 7 :(得分:4)

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

答案 8 :(得分:4)

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

通常你不应该从例如IMyInterface到MyClass。关于接口的好处是,如果你把一个对象作为实现接口的输入,那么你不必关心你得到什么样的对象。

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

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

答案 9 :(得分:3)

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

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

选择使用哪种取决于具体情况。首先,问题是您是否要在转换失败时抛出异常。

答案 10 :(得分:1)

我的答案只是关于我们不检查类型的情况下的速度,并且我们在铸造后不检查空值。我在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

不要试图专注于速度(正如我所做的那样),因为所有这一切都非常快。

答案 11 :(得分:1)

除了这里已经公开的所有内容之外,我只是遇到了一个实际的区别,我认为值得注意的是,在显式铸造之间

var x = (T) ...

与使用as运算符相比。

以下是示例:

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不适用于结构类型。 GenericCaster会。

答案 12 :(得分:1)

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

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为目标时,

强制转换就可以了:

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'

答案 13 :(得分:0)

这取决于,您是否想在使用“as”后检查null,或者您希望您的应用程序抛出异常?

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

答案 14 :(得分:0)

您的选择很大程度上取决于所需要的内容。 我更喜欢显式铸造

IMyInterface = (IMyInterface)someobj;

因为如果对象应该是IMyInterface类型而不是 - 那肯定是问题。 最好尽早得到错误,因为确切的错误将被修复,而不是修复其副作用。

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

答案 15 :(得分:0)

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

答案 16 :(得分:0)

答案 17 :(得分:0)

OP的问题仅限于特定的施法情况。标题涵盖更多情况 以下是我目前可以想到的所有相关铸造情况的概述:

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
}