强制转换vs使用'as'关键字在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);
}
}

另外,“一般来说”首选的方法是什么?

84967 次浏览

如果不能强制转换,”为“将返回NULL。

铸造之前将引发异常。

对于性能而言,引发异常的代价通常更大。

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

如果作为不能执行返回的转换,则从不抛出异常(作为仅对引用类型起作用)。所以使用作为基本上相当于

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

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

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

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

如果你正在为一个接口编程,你不应该需要强制转换。希望这种类型的演员很少。如果不是,您可能需要重新考虑一些接口。

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

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

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

这取决于,你想检查null后使用"as"或你更喜欢你的应用程序抛出一个异常?

我的经验法则是,如果我总是期望变量在我想要的时候是我期望的类型,我就使用强制转换。如果变量可能不会强制转换为我想要的,并且我准备从使用as处理空值,那么我将使用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
    }
    

    这不仅是检查两次,而且如果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;
    
  • If randomObject might be an instance of TargetType and TargetType is a reference type, then use code like this:

    TargetType convertedRandomObject = randomObject as TargetType;
    if (convertedRandomObject != null)
    {
    // Do stuff with convertedRandomObject
    }
    
  • If randomObject might be an instance of TargetType and TargetType is a value type, then we can't use as with TargetType itself, but we can use a nullable type:

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

    (注意:目前是实际上比is + cast慢。我认为它更优雅和一致,但我们继续。)

  • 如果你真的不需要转换后的值,但你只需要知道它是否是TargetType的一个实例,那么is操作符就是你的朋友。在这种情况下,TargetType是引用类型还是值类型并不重要。

  • 可能还有其他涉及泛型的情况,其中is是有用的(因为你可能不知道T是否是引用类型,所以你不能使用as),但它们相对晦涩。

  • 在此之前,我几乎肯定使用了is作为值类型的情况,没有想过将可空类型和as一起使用:)


编辑:请注意,上面没有讨论性能,除了值类型的情况,其中我已经注意到,解盒到可空值类型实际上更慢——但一致。

根据naask的回答,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检查。)
  • 他们都疯狂的快。这个简单的不会将成为你的代码中的瓶颈,除非你真的不打算在后面对这些值执行任何东西

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

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

我还认为,“当时为空”检查提供了更好的关注点分离。一个语句尝试进行转换,另一个语句使用转换结果。is-and-cast或is-and-as执行测试和然后另一次尝试转换值。

换句话说,任何人会写:

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

这就是is-and-cast正在做的事情——尽管显然是以一种相当便宜的方式。

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

通常你不需要强制转换一个对象,例如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;

因为if对象应该是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%以上)

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

请忽略Jon Skeet的建议,re:避免测试-强制转换模式,即:

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
}

这是一种不起作用的微观优化。我运行了一些真正的测试,测试-强制转换实际上比强制转换-空比较更快,而且它也更安全,因为如果强制转换失败,你不可能在if之外的作用域中有空引用。

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

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

复杂:为什么测试-强制转换更快:强制转换-空测试将另一个变量引入外部作用域,编译器必须跟踪该变量,并且它可能无法优化掉该变量,这取决于你的控制流有多复杂。相反,“测试-强制转换”只在分隔的作用域内引入新变量,这样编译器就知道该变量在作用域退出后失效,从而更好地优化寄存器分配。

所以,请让这个“强制转换-空测试比测试-强制转换更好”的建议死掉吧。请。测试-强制转换既安全又快速。

我的答案只是在我们不检查类型和转换后不检查null的情况下的速度。我在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

不要像我一样专注于速度,因为所有这些都是非常非常快的。

除了这里已经暴露的所有内容,我刚刚发现了一个我认为值得注意的实际差异,在显式选角之间

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的Office PIAs。X你应该使用作为关键字,否则它将无法编译。

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的误差为:

缺少编译器需要的成员'Microsoft.CSharp.RuntimeBinder.Binder.Convert'

缺少编译器需要的成员'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
}