在编程接口时,我发现我正在进行大量的转换或对象类型转换.
这两种转换方法有区别吗?如果是,是否有成本差异或这对我的计划有何影响?
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
后跟一个强制转换.
到目前为止,我认为没有任何答案(在开始这个答案时!)已经真正解释了哪些值得使用哪个.
不要这样做:
// 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
是值类型,那么我们就不能使用as
与TargetType
本身,但我们可以用一个可空类型:
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 }
这就是什么是演员正在做的事情 - 虽然显然是以相当便宜的方式.
如果无法进行转换,"as"将返回NULL.
之前施放将引发异常.
对于性能而言,提出异常通常会更加昂贵.
这是另一个答案,有一些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无论如何)的推荐
两者之间更微妙的区别之一是,当涉及强制转换操作符时,"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;
虽然这条线路工作得很好.
作为从未抛出异常,如果它不能执行转换返回零代替(如仅引用类型进行操作).所以使用as基本上相当于
_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;
另一方面,C风格的强制转换会在无法进行转换时抛出异常.
不是你的问题的答案,但我认为这是一个重要的相关点.
如果您正在编程接口,则不需要进行转换.希望这些演员阵容非常罕见.如果不是,您可能需要重新考虑一些接口.
请忽略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.请.测试和铸造既安全又快捷.