编写高质量代码的50条黄金守则-Day 03(首选is或as而不是强制类型转换)

在 .net 中包含三种基本的类型转换,is 操作符转换,as 操作符转换,强制类型转换,这三种类型转换各有不同却又各有联系。使用不当,可能引发 NullPointerException 异常或 InvalidCastException 异常。本文将通过一些演示案例为大家一探究竟。

编写高质量代码的50条黄金守则-Day 03(首选is或as而不是强制类型转换),本文由比特飞原创发布,转载务必在文章开头附带链接:https://www.byteflying.com/archives/6710

该系列文章由比特飞原创发布,计划用半年时间写完全50篇文章,为大家提供编写高质量代码的一般准则。

1、概述

在 .net 中包含三种基本的类型转换,is 操作符转换,as 操作符转换,强制类型转换,这三种类型转换各有不同却又各有联系。使用不当,可能引发 NullPointerException 异常或 InvalidCastException 异常。本文将通过一些演示案例为大家一探究竟。

2、通过反编译查看IL,探究类型转换的本质

接下来,我们先来准备环境:

namespace EffectiveCoding03 {

    public class Program {

        public class TypeBase {

        }

        public class TypeSub : TypeBase {

        }

        public class TypeThree {

        }

        public static void Main(string[] args) {
            TestIs();
            TestAs();
            TestConvert();
            TestUserConvert();
            TestIteration();
            TestLinq();
        }

    }

}

TypeSub 继承自 TypeBase,TypeThree 为另外一种类型。

1、is 关键字转换

再看看 TestIs 方法:

public static void TestIs() {
    var foo = new TypeSub();

    if (foo is TypeSub) {
        Console.WriteLine("foo is TypeSub => success");
    }
    else {
        Console.WriteLine("foo is TypeSub => failure");
    }

    if (foo is TypeBase) {
        Console.WriteLine("foo is TypeBase => success");
    }
    else {
        Console.WriteLine("foo is TypeBase => failure");
    }
}

先使用 is 测试变量 foo 的类型,再根据结果输出测试结果,以下是输出结果:

foo is TypeSub => success
foo is TypeBase => success

结果不出意外,均能命中,因为子类的类型能匹配本身类型,也能匹配其父类型。接下来我们看看它们的 IL:

// Token: 0x06000002 RID: 2 RVA: 0x00002070 File Offset: 0x00000270
.method public hidebysig static 
	void TestIs () cil managed 
{
	// Header Size: 12 bytes
	// Code Size: 80 (0x50) bytes
	// LocalVarSig Token: 0x11000001 RID: 1
	.maxstack 2
	.locals init (
		[0] class EffectiveCoding03.Program/TypeSub foo,
		[1] bool,
		[2] bool
	)

	/* (27,37)-(27,38) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000027C 00           */ IL_0000: nop
	/* (28,13)-(28,37) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000027D 7307000006   */ IL_0001: newobj    instance void EffectiveCoding03.Program/TypeSub::.ctor()
	/* 0x00000282 0A           */ IL_0006: stloc.0
	/* (30,13)-(30,32) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000283 06           */ IL_0007: ldloc.0
	/* 0x00000284 14           */ IL_0008: ldnull
	/* 0x00000285 FE03         */ IL_0009: cgt.un
	/* 0x00000287 0B           */ IL_000B: stloc.1
	/* (hidden)-(hidden) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000288 07           */ IL_000C: ldloc.1
	/* 0x00000289 2C0F         */ IL_000D: brfalse.s IL_001E

	/* (30,33)-(30,34) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000028B 00           */ IL_000F: nop
	/* (31,17)-(31,64) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000028C 721B000070   */ IL_0010: ldstr     "foo is TypeSub => success"
	/* 0x00000291 280B00000A   */ IL_0015: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x00000296 00           */ IL_001A: nop
	/* (32,13)-(32,14) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000297 00           */ IL_001B: nop
	/* (hidden)-(hidden) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000298 2B0D         */ IL_001C: br.s      IL_002B

	/* (33,18)-(33,19) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000029A 00           */ IL_001E: nop
	/* (34,17)-(34,64) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000029B 724F000070   */ IL_001F: ldstr     "foo is TypeSub => failure"
	/* 0x000002A0 280B00000A   */ IL_0024: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000002A5 00           */ IL_0029: nop
	/* (35,13)-(35,14) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002A6 00           */ IL_002A: nop

	/* (37,13)-(37,33) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002A7 06           */ IL_002B: ldloc.0
	/* 0x000002A8 14           */ IL_002C: ldnull
	/* 0x000002A9 FE03         */ IL_002D: cgt.un
	/* 0x000002AB 0C           */ IL_002F: stloc.2
	/* (hidden)-(hidden) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002AC 08           */ IL_0030: ldloc.2
	/* 0x000002AD 2C0F         */ IL_0031: brfalse.s IL_0042

	/* (37,34)-(37,35) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002AF 00           */ IL_0033: nop
	/* (38,17)-(38,65) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002B0 7283000070   */ IL_0034: ldstr     "foo is TypeBase => success"
	/* 0x000002B5 280B00000A   */ IL_0039: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000002BA 00           */ IL_003E: nop
	/* (39,13)-(39,14) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002BB 00           */ IL_003F: nop
	/* (hidden)-(hidden) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002BC 2B0D         */ IL_0040: br.s      IL_004F

	/* (40,18)-(40,19) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002BE 00           */ IL_0042: nop
	/* (41,17)-(41,65) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002BF 72B9000070   */ IL_0043: ldstr     "foo is TypeBase => failure"
	/* 0x000002C4 280B00000A   */ IL_0048: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000002C9 00           */ IL_004D: nop
	/* (42,13)-(42,14) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002CA 00           */ IL_004E: nop

	/* (43,9)-(43,10) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002CB 2A           */ IL_004F: ret
} // end of method Program::TestIs

IL 的代码有些疑惑,我们利用 dnSpy 反编译看看结果:

// EffectiveCoding03.Program
// Token: 0x06000002 RID: 2 RVA: 0x00002070 File Offset: 0x00000270
public static void TestIs()
{
	Program.TypeSub foo = new Program.TypeSub();
	bool flag = foo != null;
	if (flag)
	{
		Console.WriteLine("foo is TypeSub => success");
	}
	else
	{
		Console.WriteLine("foo is TypeSub => failure");
	}
	bool flag2 = foo != null;
	if (flag2)
	{
		Console.WriteLine("foo is TypeBase => success");
	}
	else
	{
		Console.WriteLine("foo is TypeBase => failure");
	}
}

结果是否令你大吃一惊,反编译结果显示 foo is TypeSub 被编译器转换为 bool flag = foo != null;,这有力的向我们证明了 is 其实为语法糖,在编译期间已经被写进 IL中。

2、as 关键字转换

使用相同的方式测试 TestAs 方法,我们完全得到一致的结论。

public static void TestAs() {
    var foo = new TypeSub();

    //var foo2 = foo as TypeThree; //编译时错误

    var foo3 = foo as TypeBase;

    if (foo3 != null) {
        Console.WriteLine("foo3 as TypeBase => success");
    }
    else {
        Console.WriteLine("foo3 as TypeBase => failure");
    }
}
// Token: 0x06000003 RID: 3 RVA: 0x000020CC File Offset: 0x000002CC
.method public hidebysig static 
	void TestAs () cil managed 
{
	// Header Size: 12 bytes
	// Code Size: 46 (0x2E) bytes
	// LocalVarSig Token: 0x11000002 RID: 2
	.maxstack 2
	.locals init (
		[0] class EffectiveCoding03.Program/TypeSub foo,
		[1] class EffectiveCoding03.Program/TypeBase foo3,
		[2] bool
	)

	/* (45,37)-(45,38) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002D8 00           */ IL_0000: nop
	/* (46,13)-(46,37) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002D9 7307000006   */ IL_0001: newobj    instance void EffectiveCoding03.Program/TypeSub::.ctor()
	/* 0x000002DE 0A           */ IL_0006: stloc.0
	/* (50,13)-(50,40) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002DF 06           */ IL_0007: ldloc.0
	/* 0x000002E0 0B           */ IL_0008: stloc.1
	/* (52,13)-(52,30) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002E1 07           */ IL_0009: ldloc.1
	/* 0x000002E2 14           */ IL_000A: ldnull
	/* 0x000002E3 FE03         */ IL_000B: cgt.un
	/* 0x000002E5 0C           */ IL_000D: stloc.2
	/* (hidden)-(hidden) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002E6 08           */ IL_000E: ldloc.2
	/* 0x000002E7 2C0F         */ IL_000F: brfalse.s IL_0020

	/* (52,31)-(52,32) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002E9 00           */ IL_0011: nop
	/* (53,17)-(53,66) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002EA 72EF000070   */ IL_0012: ldstr     "foo3 as TypeBase => success"
	/* 0x000002EF 280B00000A   */ IL_0017: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x000002F4 00           */ IL_001C: nop
	/* (54,13)-(54,14) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002F5 00           */ IL_001D: nop
	/* (hidden)-(hidden) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002F6 2B0D         */ IL_001E: br.s      IL_002D

	/* (55,18)-(55,19) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002F8 00           */ IL_0020: nop
	/* (56,17)-(56,66) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x000002F9 7227010070   */ IL_0021: ldstr     "foo3 as TypeBase => failure"
	/* 0x000002FE 280B00000A   */ IL_0026: call      void [System.Console]System.Console::WriteLine(string)
	/* 0x00000303 00           */ IL_002B: nop
	/* (57,13)-(57,14) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000304 00           */ IL_002C: nop

	/* (58,9)-(58,10) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000305 2A           */ IL_002D: ret
} // end of method Program::TestAs
// EffectiveCoding03.Program
// Token: 0x06000003 RID: 3 RVA: 0x000020CC File Offset: 0x000002CC
public static void TestAs()
{
	Program.TypeSub foo = new Program.TypeSub();
	Program.TypeBase foo2 = foo;
	bool flag = foo2 != null;
	if (flag)
	{
		Console.WriteLine("foo3 as TypeBase => success");
	}
	else
	{
		Console.WriteLine("foo3 as TypeBase => failure");
	}
}

反编译结果显示 var foo3 = foo as TypeBase; 被编译器转换为 bool flag = foo2 != null;,同时也证明了 as 为语法糖,在编译期间已经被写进 IL中。

3、强制类型转换

最后来测试一下 TestConvert 方法。

public static void TestConvert() {
    var foo = new TypeSub();
    var foo2 = (TypeBase)foo;

    Console.WriteLine(foo2);
}
// Token: 0x06000004 RID: 4 RVA: 0x00002108 File Offset: 0x00000308
.method public hidebysig static 
	void TestConvert () cil managed 
{
	// Header Size: 12 bytes
	// Code Size: 17 (0x11) bytes
	// LocalVarSig Token: 0x11000003 RID: 3
	.maxstack 1
	.locals init (
		[0] class EffectiveCoding03.Program/TypeSub foo,
		[1] class EffectiveCoding03.Program/TypeBase foo2
	)

	/* (60,42)-(60,43) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000314 00           */ IL_0000: nop
	/* (61,13)-(61,37) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000315 7307000006   */ IL_0001: newobj    instance void EffectiveCoding03.Program/TypeSub::.ctor()
	/* 0x0000031A 0A           */ IL_0006: stloc.0
	/* (62,13)-(62,38) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000031B 06           */ IL_0007: ldloc.0
	/* 0x0000031C 0B           */ IL_0008: stloc.1
	/* (64,13)-(64,37) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x0000031D 07           */ IL_0009: ldloc.1
	/* 0x0000031E 280C00000A   */ IL_000A: call      void [System.Console]System.Console::WriteLine(object)
	/* 0x00000323 00           */ IL_000F: nop
	/* (65,9)-(65,10) C:\Users\Administrator\source\repos\EffectiveCoding03\EffectiveCoding03\Program.cs */
	/* 0x00000324 2A           */ IL_0010: ret
} // end of method Program::TestConvert
// EffectiveCoding03.Program
// Token: 0x06000004 RID: 4 RVA: 0x00002108 File Offset: 0x00000308
public static void TestConvert()
{
	Program.TypeSub foo = new Program.TypeSub();
	Program.TypeBase foo2 = foo;
	Console.WriteLine(foo2);
}

看起来, .net 运行时在做类型转换时,只是直接将源对象直接赋值给目标对象而已?其实上编译器对as操作符、is操作符和强制类型转换都会对其合法性进行校验,他们本质上都可以看作是语法糖。于是,我们有了以下总结:

1、is 操作符和 as 操作符均为语法糖;

2、is 操作符可以使代码更为精简。

3、自定义类型转换

为了使 TypeThree 类型支持自定义类型转换,我们现将其改造如下:

public class TypeThree {

    public static implicit operator TypeSub(TypeThree typeThree) {
        throw new NotImplementedException();//具体的自定义类型转换实现
    }

}

具体的自定义类型转换请在实际开发中自行实现。以上代码其实是强制类型转换的运算符重载,我们重载了 TypeSub 运算符,即可使类似于 var foo = (TypeSub) someObject; 这样的代码可以在编译器编译期间通过其合法性校验,并在运行时使用重载的自定义类型转换去转换相应的类型。下面我们来测试一下:

public static void TestUserConvert() {
    var foo = new TypeThree();
    var foo2 = (TypeSub)foo;

    Console.WriteLine(foo2);
}

注意 var foo2 = (TypeSub)foo; 这行代码若没有 public static implicit operator TypeSub(TypeThree typeThree) 这个自定义类型转换的话,编译期间就会报错,而有了这个转换,编译器才知道如何处理它。我们来看看解码的 IL 验证一下我们的想法:

编写高质量代码的50条黄金守则-Day 03(首选is或as而不是强制类型转换)
自定义类型转换的 IL
编写高质量代码的50条黄金守则-Day 03(首选is或as而不是强制类型转换)
IL 中调用自定义类型转换

通过以上2张截图的红框部分,我们明显可以看出运行时在进行强制类型转换时调用了自定义强制类型转换所提供的转换方法。由于我们有了以下总结:

1、自定义类型转换提供了更为灵活的类型转换方案;

2、is 操作符和 as 操作符没有任何机会执行自定义类型转换;

3、强制类型转换需要手工捕获异常,is 操作符和 as 操作符只需要进行 null 判定。

4、循环中的类型转换问题

循环中的类型转换问题和强制类型转换基本一致,我们看一下示例:

private static IEnumerable<TypeThree> GetTypeThrees() {
    //仅为测试使用
    yield return new TypeThree();
    yield return new TypeThree();
    yield return new TypeThree();
}

public static void TestIteration() {
    var threes = GetTypeThrees();

    foreach (TypeSub sub in threes) {
        Console.WriteLine(sub.ToString());
    }
}

以上迭代中的代码与以下代码基本等价:

public static void TestIteration() {
    var threes = GetTypeThrees();
    var it = threes.GetEnumerator();

    while (it.MoveNext()) {
        var tp = (TypeSub)it.Current;
        Console.WriteLine(tp.ToString());
    }
}
编写高质量代码的50条黄金守则-Day 03(首选is或as而不是强制类型转换)

解码后的 IL 中显示,foreach (TypeSub sub in threes) 与普通的类型转换并无二致。

5、结合泛型和Linq进行类型转换

结合泛型和 Linq 进行类型转换时需要注意的是,Linq 本质上是一系列定义好的扩展方法,所以结合 Linq 使用类型转换时,无需提供自定义的类型转换方法也可以被顺利编译。但是要小心的是,如果被转换的类型不是源类型的子类或其它兼容数据类型,则可能导致运行时异常。

public static void TestLinq() {
    var threes = GetTypeThrees();

    var result = threes.Cast<TypeSub>();
}

以上代码中的 var result = threes.Cast(); ,无论是否提供 public static implicit operator TypeSub(TypeThree typeThree) ,均可在编译时顺利通过。

6、总结

1、is 操作符和 as 操作符均为语法糖;

2、is 操作符可以使代码更为精简;

3、自定义类型转换提供了更为灵活的类型转换方案;

4、is 操作符和 as 操作符没有任何机会执行自定义类型转换;

5、强制类型转换需要手工捕获异常,is 操作符和 as 操作符只需要进行 null 判定;

6、Linq 中的 Cast of T 转换不需要自定义类型转换重载,但必须是源类型的子类或其它兼容数据类型。

开发人员应牢记以上开发守则,否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你、唾弃你

该系列文章由比特飞原创发布,计划用半年时间写完全50篇文章,为大家提供编写高质量代码的一般准则。

本文由 .Net中文网 原创发布,欢迎大家踊跃转载。

转载请注明本文地址:https://www.byteflying.com/archives/6710

发表评论

登录后才能评论