当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《使用CLR中的'as'关键字进行转换》经验,为你挑选了7个好方法。

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

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

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

另外,什么是"一般"首选方法?



1> Jon Skeet..:

线下的答案写于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,即如果它不是,这意味着有一个错误,那么转换是正确的解决方案.这会立即引发异常,这意味着在不正确的假设下不再进行任何工作,并且异常正确地显示了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;

如果randomObject 可能TargetType并且TargetType是引用类型的实例,则使用如下代码:

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

如果randomObject 可能是一个实例TargetType并且TargetType是值类型,那么我们就不能使用asTargetType本身,但我们可以用一个可空类型:

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

(注意:目前这实际上比+ +更慢.我认为它更优雅和一致,但我们去了.)

如果您确实不需要转换后的值,但只需要知道它是否 TargetType的实例,那么is运算符就是您的朋友.在这种情况下,TargetType是引用类型还是值类型无关紧要.

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

我以前几乎肯定用过is值类型的情况,没有想到使用可空类型和as一起:)


编辑:请注意,上面没有谈到性能,除了值类型的情况,我已经注意到拆箱到可以为空的值类型实际上更慢 - 但一致.

根据naasking的回答,is-and-cast或is-and-as与现代JIT一样快和as-and-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);
    }
}

在我的笔记本电脑上,这些都在大约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执行测试,然后再尝试转换值.

换个方式,会有人写:

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

这就是什么是演员正在做的事情 - 虽然显然是以相当便宜的方式.


以下是IL/as/cast的成本:http://www.atalasoft.com/cs/blogs/stevehawley/archive/2009/01/30/is-as-and-casting.aspx
如果targetObject _might_是目标类型,为什么使用"is"和cast组合被认为是一种不好的做法?我的意思是,它生成一个较慢的代码,但在这种情况下,意图比AS强制转换更明确,如"如果targetObject是targetType则执行某些操作",而不是"如果targetObject为null则执行某些操作",而AS子句将创建一个不必要的变量超出IF范围.
@Valera:好点,虽然我建议as/null测试足够惯用,几乎所有C#开发人员都应该明白这个意图.我个人并不喜欢is + cast中涉及的重复.我实际上喜欢一种"as-if"构造,它将两种动作合二为一.他们经常一起去......
@Jon Skeet:对不起我迟到了.是和演员:2135,是和作为:2145,As和null检查:1961,规格:操作系统:Windows 7,CPU:i5-520M,4GB DDR3 1033 ram,阵列基准128,000,000件物品.
使用C#7,你可以这样做:`if(randomObject是TargetType convertedRandomObject){//使用convertedRandomObject.Value做的东西}`或者使用`switch` /`case` [见文档](https://docs.microsoft.com/ EN-US/DOTNET/CSHARP /语言参考/关键字/开关)

2> Patrick Desj..:

如果无法进行转换,"as"将返回NULL.

之前施放将引发异常.

对于性能而言,提出异常通常会更加昂贵.


可能引发异常的成本是需要考虑的因素,但通常是正确的设计.
@Frank - 例如,如果您需要使用pre-generics集合,并且API中的方法需要Employees列表,而某些joker会传递Products列表,那么无效的强制转换异常可能适合发出信号违反了接口要求.
异常提高成本更高,但如果您知道对象可以正确转换,那么由于安全检查,_as_需要更多时间(请参阅Anton的回复).但是,我认为,安全检查的成本非常小.

3> Chris S..:

这是另一个答案,有一些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无论如何)的推荐



4> Patrik Hägne..:

两者之间更微妙的区别之一是,当涉及强制转换操作符时,"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;虽然这条线路工作得很好.



5> Anton Gogole..:

作为从未抛出异常,如果它不能执行转换返回代替(仅引用类型进行操作).所以使用as基本上相当于

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

另一方面,C风格的强制转换会在无法进行转换时抛出异常.


等价,是的,但不一样.这会生成更多代码.

6> toad..:

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

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



7> naasking..:

请忽略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
}

这是微观优化,不起作用.我运行了一些真正的测试,测试和转换实际上比cast-and-null-comparison更快,并且它也更安全,因为你不可能在范围之外的范围内使用null引用失败.

如果你想要为什么测试和转换更快,或者至少不慢,这是一个简单而复杂的原因.

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

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

所以请,请让这个"演员和空测试比测试和演员更好"的建议DIE.请.测试和铸造既安全又快捷.


@naasking:如果你测试两次(根据你的第一个片段),那么两种测试之间的类型可能会发生变化,如果它是一个字段或`ref`参数.它对局部变量是安全的,但不适用于字段.我有兴趣运行您的基准测试,但您在博客文章中提供的代码并不完整.我同意不是微优化,但我不认为使用该值两次比使用"as"和无效测试更可读或更优雅.(顺便说一句,我绝对会使用直接演员而不是"as".)
我也不明白为什么它更安全.事实上,我已经说明了为什么它不那么安全了.当然,你最终得到的范围变量可能为null,但除非你开始在后续"if"块的范围之外使用它,否则你没事.我提出的安全问题(围绕字段改变其价值)是一个真正的问题*显示的代码* - 您的安全问题要求开发人员在其他代码中松懈.
推荐阅读
贴进你的心聆听你的世界
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有