提问人:Frank V 提问时间:1/31/2009 最后编辑:Frank V 更新时间:8/14/2017 访问量:87659
强制转换与在 CLR 中使用“as”关键字
Casting vs using the 'as' keyword in the 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);
}
}
另外,“一般”首选方法是什么?
答:
如果无法强制转换,“as”将返回 NULL。
cast before 将引发异常。
对于性能而言,引发异常通常在时间上成本更高。
评论
如果强制转换失败,则“as”关键字不会引发异常;它将变量设置为 null(或值类型的默认值)。
评论
如果 AS 无法执行返回 NULL 的转换,则从不引发异常(AS 仅对引用类型进行操作)。所以使用 as 基本上等同于
_myCls2 = _myObj is MyClass ? (MyClass)_myObj : null;
另一方面,C 样式强制转换在无法转换时会引发异常。
评论
该关键字的工作方式与兼容引用类型之间的显式强制转换相同,主要区别在于,如果转换失败,它不会引发异常。相反,它会在目标变量中生成一个 null 值。由于 Exceptions 在性能方面非常昂贵,因此它被认为是一种更好的强制转换方法。as
评论
不是你问题的答案,但我认为这是一个重要的相关点。
如果要对接口进行编程,则不需要强制转换。希望这些演员阵容非常罕见。如果没有,您可能需要重新考虑某些界面。
评论
运算符只能用于引用类型,不能重载,如果操作失败,它将返回。它永远不会抛出异常。as
null
强制转换可用于任何兼容类型,它可以重载,如果操作失败,它将引发异常。
选择使用哪种取决于具体情况。首先,这是您是否要在失败的转换上引发异常的问题。
评论
这取决于,您是想在使用“as”后检查 null,还是希望您的应用程序抛出异常?
我的经验法则是,如果我总是希望变量是我想要的类型,那么我就使用强制转换。如果变量可能无法转换为我想要的内容,并且我准备处理使用 as 的 null,我将使用 as。
这句话下面的答案写于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 }
这不仅是两次检查,而且如果是字段而不是局部变量,它可能会检查不同的东西。如果另一个线程更改了两者之间的值,则“if”可能会通过,但强制转换会失败。
randomObject
randomObject
如果真的应该是 的实例,即如果不是,则意味着存在错误,那么强制转换是正确的解决方案。这会立即引发异常,这意味着在不正确的假设下不会再做任何工作,并且异常会正确显示 bug 的类型。
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 }
如果可能是 和 的实例,则不能与自身一起使用,但我们可以使用可为 null 的类型:
randomObject
TargetType
TargetType
as
TargetType
TargetType? convertedRandomObject = randomObject as TargetType?; if (convertedRandomObject != null) { // Do stuff with convertedRandomObject.Value }
(注意:目前这实际上比 is + cast 慢。我认为它更优雅、更一致,但我们开始了。
如果你真的不需要转换后的值,但你只需要知道它是否是 TargetType 的实例,那么运算符就是你的朋友。在这种情况下,TargetType 是引用类型还是值类型并不重要。
is
可能还有其他涉及泛型的情况很有用(因为您可能不知道 T 是否是引用类型,因此您不能使用 as),但它们相对模糊。
is
我几乎可以肯定之前已经使用过值类型情况,没有想过使用可为 null 的类型并一起:)
is
as
编辑:请注意,除了值类型情况外,以上内容都不涉及性能,我注意到将装箱为可空的值类型实际上较慢 - 但始终如一。
根据 naasking 的答案,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-check。
- 它们的速度都非常快。这根本不会成为代码中的瓶颈,除非您之后真的不打算对这些值执行任何操作。
因此,我们不必担心性能。让我们担心正确性和一致性。
我坚持认为,在处理变量时,is-and-cast(或is-and-as)都是不安全的,因为它所引用的值的类型可能会由于测试和转换之间的另一个线程而改变。这将是一个非常罕见的情况 - 但我宁愿有一个我可以始终如一地使用的约定。
我还坚持认为,as-then-null-检查可以更好地分离关注点。我们有一个语句尝试转换,然后有一个语句使用结果。is-and-cast 或 is-and-as 执行测试,然后再次尝试转换值。
换句话说,有人会写:
int value;
if (int.TryParse(text, out value))
{
value = int.Parse(text);
// Use value
}
这就是 is-and-cast 正在做的事情——尽管显然是以一种相当便宜的方式。
评论
if (randomObject is TargetType convertedRandomObject){ // Do stuff with convertedRandomObject.Value}
switch
case
这不是对问题的回答,而是对问题代码示例的注释:
通常,您不必将对象从 IMyInterface 转换为 MyClass。接口的伟大之处在于,如果你将一个对象作为实现接口的输入,那么你就不必关心你得到的是什么样的对象。
如果将 IMyInterface 强制转换为 MyClass,则已经假设您获得了 MyClass 类型的对象,并且使用 IMyInterface 是没有意义的,因为如果使用实现 IMyInterface 的其他类来提供代码,则会破坏您的代码......
现在,我的建议是:如果你的界面设计得很好,你可以避免大量的类型转换。
两者之间更微妙的区别之一是,当涉及强制转换运算符时,“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;
你选择什么很大程度上取决于需要什么。 我更喜欢显式铸造
IMyInterface = (IMyInterface)someobj;
因为如果对象应该是 IMyInterface 类型,而它不是 - 这肯定是问题。 最好尽早获得错误,因为精确的错误将被修复,而不是修复其副作用。
但是,如果您处理接受作为参数的方法,则需要在执行任何代码之前检查其确切类型。在这种情况下会很有用,因此您可以避免.object
as
InvalidCastException
这是另一个答案,有一些 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)推荐的
评论
请忽略 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
}
这是一种行不通的微优化。我运行了一些实际测试,测试和强制转换实际上比强制转换和 null 比较更快,而且它也更安全,因为如果强制转换失败,您不可能在 if 之外的范围内有 null 引用。
如果你想要一个测试和投射速度更快,或者至少不慢的原因,有一个简单而复杂的原因。
很简单:即使是幼稚的编译器也会将两个类似的操作(如测试和强制转换)合并到一个测试和分支中。cast-and-null-test 可能会强制执行两个测试和一个分支,一个用于类型测试并在失败时转换为 null,另一个用于 null 检查本身。至少,它们都将针对单个测试和分支进行优化,因此测试和强制转换既不比强制转换和 null 测试慢也不快。
复杂:为什么测试和强制转换速度更快:强制转换和空测试将另一个变量引入外部范围,编译器必须跟踪该变量的活跃度,并且它可能无法根据控制流的复杂程度优化该变量。相反,test-and-cast 仅在分隔作用域中引入新变量,因此编译器知道该变量在作用域退出后已失效,因此可以更好地优化寄存器分配。
所以,请让这个“铸造和空测试比测试和铸造更好”的建议 DIE。请。测试和投射既安全又快捷。
评论
ref
请看一下这些链接:
- 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
它们向您展示了一些详细信息和性能测试。
我的答案只是关于速度,如果我们不检查类型,并且在强制转换后不检查空值。我在 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
除了这里已经暴露的所有内容之外,我刚刚遇到了一个我认为值得注意的实际差异,即明确的选角
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 将。
如果使用面向 .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”
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
}
评论