在编程接口时,我发现我正在做很多转换或对象类型转换。
这两种转换方法有区别吗?如果是这样,是否存在成本差异或这对我的计划有何影响?
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
后跟一个演员表。
到目前为止,我认为没有任何答案(在开始此答案时!)真正解释了值得在哪里使用哪个。
不要这样做: // 错误代码 - 无缘无故检查类型两次 if (randomObject is TargetType) { TargetType foo = (TargetType) randomObject; // 用 foo 做一些事情 } 这不仅检查了两次,而且如果 randomObject 是一个字段而不是一个局部变量,它可能会检查不同的东西。如果另一个线程在两者之间更改了 randomObject 的值,则“if”可能会通过但强制转换失败。
如果randomObject 真的应该是TargetType 的一个实例,即如果不是,则意味着存在错误,那么强制转换是正确的解决方案。这会立即引发异常,这意味着在不正确的假设下不再进行任何工作,并且异常正确地显示了错误的类型。 // 如果 randomObject 为非 null 并且 // 引用了不兼容类型的对象,这将引发异常。 // 如果这是您想要的行为,演员表是最好的代码。 TargetType convertRandomObject = (TargetType) randomObject;
如果 randomObject 可能是 TargetType 的实例并且 TargetType 是引用类型,则使用如下代码: TargetType convertRandomObject = randomObject as TargetType; if (convertedRandomObject != null) { // 用 convertRandomObject 做事 }
如果 randomObject 可能是 TargetType 的实例,而 TargetType 是值类型,那么我们不能与 TargetType 本身一样使用,但我们可以使用可为空的类型:TargetType? convertRandomObject = randomObject 作为 TargetType?; if (convertedRandomObject != null) { // 使用 convertRandomObject.Value } (注意:目前这实际上比 is + cast 慢。我认为它更优雅和一致,但我们开始了。)
如果您确实不需要转换后的值,而只需要知道它是否是 TargetType 的实例,那么 is 运算符就是您的朋友。在这种情况下,TargetType 是引用类型还是值类型都没有关系。
可能还有其他涉及泛型的情况是有用的(因为您可能不知道 T 是否是引用类型,所以您不能使用 as)但它们相对模糊。
我几乎可以肯定之前使用的是值类型的情况,没有想过使用可空类型和一起使用:)
编辑:请注意,除了值类型案例之外,以上都没有谈到性能,我注意到拆箱到可空值类型实际上更慢 - 但一致。
根据 naasking 的回答,is-and-cast 或 is-and-as 都与现代 JIT 的空值检查一样快,如下面的代码所示:
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-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
}
这就是 is-and-cast 正在做的事情——尽管显然是以一种相当便宜的方式。
如果无法强制转换,“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
This blog post 对这两种方法进行了很好的比较。他的总结是:
在直接比较中,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 如果它不能执行返回 null 的转换,则永远不会抛出异常(因为仅对引用类型进行操作)。所以使用 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
}
这是一个不起作用的微优化。我跑了 some real tests,测试和转换实际上比转换和空比较快,而且它也更安全,因为你不可能在 if 应该转换的范围内有一个空引用失败。
如果你想知道为什么 test-and-cast 更快,或者至少不慢,有一个简单而复杂的原因。
很简单:即使是简单的编译器也会将两个类似的操作(如测试和转换)合并为一个测试和分支。 cast-and-null-test 可能会强制执行两个测试和一个分支,一个用于类型测试并在失败时转换为 null,另一个用于 null 检查本身。至少,它们都将优化到单个测试和分支,因此测试和转换不会比转换和空测试慢也不会快。
复杂:为什么 test-and-cast 更快:cast-and-null-test 将另一个变量引入外部范围,编译器必须跟踪该变量的活性,并且它可能无法优化该变量,具体取决于您的控件的复杂程度 -流是。相反,test-and-cast 只在分隔范围内引入新变量,因此编译器知道该变量在范围退出后已死,因此可以更好地优化寄存器分配。
所以,请让这个“cast-and-null-test is better than test-and-cast”的建议消亡。请。 test-and-cast 既安全又快速。
ref
参数。它对局部变量是安全的,但对字段不安全。我有兴趣运行您的基准测试,但您在博客文章中提供的代码并不完整。我同意不进行微优化,但我不认为使用两次值比使用“as”和无效测试更具可读性或优雅性。 (顺便说一句,我肯定会在 is 之后使用直接转换而不是“as”。)
如果转换失败,'as' 关键字不会抛出异常;而是将变量设置为 null(或值类型的默认值)。
这不是问题的答案,而是对问题代码示例的评论:
通常,您不必将对象从例如 IMyInterface 转换为 MyClass。接口的好处在于,如果您将一个对象作为实现接口的输入,那么您就不必关心您获得的是哪种对象。
如果你将 IMyInterface 转换为 MyClass,那么你已经假设你得到了一个 MyClass 类型的对象,并且使用 IMyInterface 没有任何意义,因为如果你用其他实现 IMyInterface 的类来提供你的代码,它会破坏你的代码......
现在,我的建议是:如果你的界面设计得很好,你可以避免大量的类型转换。
as
运算符只能用于引用类型,不能重载,如果操作失败将返回 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
不要试图专注于速度(就像我所做的那样),因为这一切都非常非常快。
as
转换(没有错误检查)的运行速度比强制转换快 1-3%(大约 540 毫秒,而 1 亿次迭代为 550 毫秒)。两者都不会成功或破坏您的应用程序。
如果您使用面向 .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”
你选择什么很大程度上取决于需要什么。我更喜欢显式转换
IMyInterface = (IMyInterface)someobj;
因为如果对象应该是 IMyInterface 类型而不是 - 这绝对是问题。最好尽早获得错误,因为将修复确切的错误而不是修复其副作用。
但是,如果您处理接受 object
作为参数的方法,那么您需要在执行任何代码之前检查其确切类型。在这种情况下,as
会很有用,因此您可以避免使用 InvalidCastException
。
除了这里已经公开的所有内容之外,我还发现了一个我认为值得注意的实际区别,即显式转换
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 将。
as
关键字的作用与兼容引用类型之间的显式转换相同,主要区别在于它在转换失败时不会引发异常。相反,它在目标变量中产生一个空值。由于异常在性能方面非常昂贵,因此它被认为是一种更好的强制转换方法。
这取决于,您想在使用“as”后检查 null 还是希望您的应用程序抛出异常?
我的经验法则是,如果我总是希望变量是我希望使用强制转换时所期望的类型。如果变量可能不会转换为我想要的,并且我准备处理使用 as 的空值,我将使用 as。
看看这些链接:
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
他们向您展示了一些细节和性能测试。
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
}
if (randomObject is TargetType convertedRandomObject){ // Do stuff with convertedRandomObject.Value}
或使用switch
/case
see docs