你是否考虑过这个问题:为什么不同类型之间的变量可以赋值,而不需要强制转换类型?如:
int i = 1; long l = i; object obj = 1; Exception exception = new ArgumentNullException(); Array array = new string[0]; IEnumerable<int> enumerable = new List<int>();
其实这是由C#中的隐式转换去完成的。目前,C#中可用的隐式转换有下面这些:
1、标识转换
标识转换表示任何类型可以在同一类型之间任意转换,这种转换看起来是理所当然的。
其实,这种转换的由来,是由于C#4.0开始引入了dynamic,在运行时,dynamic和object可以认为是一样的,但是在编译时,编译器认为它们是不同类型,所以就需要一种转换来解决这个问题,这就是标识转换。
2、隐式数值类型转换
下面的数值之间可以隐式的转换,主要有:
1、sbyte 类型可以隐式转换为 short, int, long, float, double, decimal 2、byte 类型可以隐式转换为 short, ushort, int, uint, long, ulong, float, double, decimal 3、short 类型可以隐式转换为 int, long, float, double, decimal 4、ushort 类型可以隐式转换为 int, uint, long, ulong, float, double, decimal 5、int 类型可以隐式转换为 long, float, double, decimal 6、uint 类型可以隐式转换为 long, ulong, float, double, decimal 7、long 类型可以隐式转换为 float, double, decimal 8、ulong 类型可以隐式转换为 float, double, decimal 9、char 类型可以隐式转换为 ushort, int, uint, long, ulong, float, double, decimal 10、float 类型可以隐式转换为 double
3、隐式枚举类型转换
隐式枚举转换表示数值 0 可以自动转换为任何的枚举类型,如:
public enum DataType { String = 1, Number = 2 }//没有定义值为0的属性 static void Main(string[] args) { DataType type = 0; Console.WriteLine(type);//输出0,因为枚举类型中没有一项的值为0 }
4、隐式内插字符串转换
隐式内插字符串转换表示内插字符串可以自动转换为 System.IFormattable 或者 System.FormattableString 类型的对象,如:
FormattableString formattableString = $"Number:{123}"; IFormattable formattable = $"DayOfWeek:{DayOfWeek.Monday}"; string value = $"String:{formattable}";
5、隐式可空类型转换
首先,我们知道,int、bool等和struct结构体类型是不可以为空的,也就是不能将 null 值赋值给它们,但是C#允许我们去创建它们的可空类型(在类型后加问号?就可以了),如:int?、bool?等(等价于 Nullable<int> 和 Nullable<bool>)。
隐式可空类型转换就是指一个值类型可以隐式的转换为它的可空类型,如:
int i = 1; int? nullable_i = i; bool b = true; Nullable<bool> nullable_b = b; Guid guid = Guid.NewGuid(); Guid? nullable_guid = guid;
6、null 隐式转换
null 值可以转换为任何可以为 null 类型,如:
int? i = null; Exception exception = null;
7、隐式引用转换
隐式引用转换表示的是引用类型之间的转换,包括:
-
任何引用类型可以隐式转换为 object 和 dynamic
-
任何一个class类类型可以隐式转换为它的基类类型(包括基类的基类等)
-
任何一个class类类型可以隐式转换为它所实现的接口类型
-
任何一个接口类型可以隐式转换为它的所有父接口(包括父接口的父接口等)
-
任意两个数组,比如 array1(元素类型是 T1)和 array2(元素类型是 T2), 如果他们满足下面三个条件,那么数组 array1 可以隐式的转换为数组 array2 :
1、两个数组array1和array2有相同的维度 2、T1 和 T2 都是引用类型 3、T1 可以通过隐式引用转换成 T2
比如:
Exception[] exceptions = new ArgumentException[0][0];//不能转换,维度不同 long[] longs = new int[0];//不能转换,int、long不是引用类型 int? ints = new int[0];//不能转换,int不是引用类型 string[] strs = new int[0];//不能转换,元素类型同 Exception[] array = new ApplicationException[0];//可以转换
-
任何一个数组可以隐式的转换为
System.Array
类型及它所实现的接口类型(ICollection, IEnumerable, IList, IStructuralComparable, IStructuralEquatable, ICloneable) -
任意一个一维数组 T[] 可以隐式的转换为 System.Collections.Generic.IList<S> 及其所实现的接口类型( ICollection<S>, IEnumerable<S>, IEnumerable),但是要求 T、S 为引用类型,且 T 可以通过隐式引用类型转换成 S ,如:
IList<long> list = new int[0];//不能转换,int、long不是引用类型 IList<Exception> exceptions = new ArgumentException[0];//可以转换
-
任何一个委托类型都可以隐式的转换为
System.Delegate
和它所实现的接口(ICloneable, ISerializable),如:Action action = () => Console.WriteLine("action"); Delegate delegate1 = action; Func<bool> func = () => true; Delegate delegate2 = func;
-
null值可以隐式转换为任何引用类型
-
如果一个类型 T1 可以通过标识转换或者隐式引用转换为 T2,而 T2 可以通过标识转换转换为 T3,那么 T1 也可以隐式转换为 T3 (这也是理所当然的)
-
如果一个类型 T1 可以通过标识转换或者隐式易用转换成 T2,而 T2 可以通过Variance规则转换成 T3 ,那么 T1 也可以隐式转换为 T3
其实这一点是针对Variance的,Variance是差异性转换,通常指的是协变、逆变、不变。
在定义泛型接口类型和泛型委托类型的时候,我们可以对泛型参数指定 in 或者 out关键字(只针对接口和委托有效),也可以不指定:
协变:通过out关键字修饰的参数属于协变参数,在隐式转换时,泛型参数可以是这个泛型参数类型、它的子类、子接口等派生类,常见的比如IEnumerable<out T>、IEnumerator<out T>、IQueryable<out T> 逆变:通过in关键字修饰的参数是逆变参数,与协变相反,在隐式转换时,泛型参数可以是这个泛型参数类型、它的父类型、父接口等,常见的如:Action<in T>,Func<in T1, out T2> 不变:不使用out和in关键字,表示只能是同一个类型才能转换,如:List<T>、IList<>、Dictionary<TKey, TValue>、IDictionary<TKey, TValue>
举个例子来对比理解,我们有以下几个类和接口:
interface IGrandpa { } interface IFather : IGrandpa { } interface ISon : IFather { } class Grandpa : IGrandpa { } class Father : Grandpa, IFather { } class Son : Father, ISon { }
对于协变:
//协变,out参数指定的泛型可以使类型本身、子类型、子接口等 IEnumerable<IFather> fathers1 = new IFather[0];//泛型参数本身 IEnumerable<IFather> fathers2 = new List<Father>();//泛型参数是实现了接口的类 IEnumerable<IFather> fathers3 = new List<ISon>();//泛型参数是子接口 IEnumerable<IFather> fathers4 = new Son[0];//泛型参数是实现了子接口的类 //错误用法 IEnumerable<IFather> fathers5 = new IGrandpa[0];//报错,泛型参数不能是父接口 IEnumerable<IFather> fathers6 = new Grandpa[0];//报错,泛型参数不能是父接口的实现类
对于逆变:
//逆变,in参数指定的泛型可以使类型本身、父类型、父接口等 Action<IFather> action1 = f => Console.WriteLine(nameof(IFather)); Action<IGrandpa> action2 = f => Console.WriteLine(nameof(IGrandpa)); Action<Grandpa> action3 = f => Console.WriteLine(nameof(Grandpa)); Action<Father> action4 = f => Console.WriteLine(nameof(Father)); Action<Father> father1 = action1;//泛型参数可以使实现的接口 Action<Father> father2 = action2;//泛型参数可以是父接口 Action<Father> father3 = action3;//泛型参数可以使父类 Action<Father> father4 = action4;//泛型参数可以使本身 //错误用法 Action<ISon> action5 = f => Console.WriteLine(nameof(ISon)); Action<Son> action6 = f => Console.WriteLine(nameof(Son)); Action<Father> father5 = action5;//报错,泛型参数不能是子接口 Action<Father> father6 = action6;//报错,泛型参数不能是子类 //Fun<in T1, out T2>是逆变和协变的结合 Func<Father, IFather> func1 = f => new Father(); Func<IFather, Father> func2 = f => new Father(); Func<IGrandpa, ISon> func3 = f => new Son(); Func<IFather, Son> func4 = f => new Son(); Func<IGrandpa, Son> func5 = f => new Son(); Func<Father, IFather> ff1 = func1;//两个泛型参数都是自身 Func<Father, IFather> ff2 = func2;//输入参数是逆变,输出是协变 Func<Father, IFather> ff3 = func3;//输入参数是逆变,输出是协变 Func<Father, IFather> ff4 = func4;//输入参数是逆变,输出是协变 Func<Father, IFather> ff5 = func5;//输入参数是逆变,输出是协变
对于不变:
//不变,泛型参数必须一致 IList<IFather> father1 = new List<IFather>(); IList<Father> father2 = new List<Father>(); //错误用法 IList<IFather> father3 = new List<Father>(); IList<IFather> father4 = new List<Son>(); IList<IFather> father5 = new List<ISon>(); IList<IFather> father6 = new List<IGrandpa>(); IList<IFather> father7 = new List<Grandpa>();
8、装箱转换
装箱转换主要指的是从值类型转换为引用类型,包括:
-
任何值类型转换为object
-
任何值类型转换为System.ValueType类(所有的struct类均派生自System.ValueType)
-
任何非空的值类型转换为它们所实现的接口
-
任何可空的值类型转换为它非空值类型所实现的接口
-
任何的枚举类型转换为
System.Enum
-
任何可空的枚举类型转换为
System.Enum
举个例子:
//任何值类型转换为object object obj1 = 1;//int装箱为object object obj2 = false;//bool装箱为object
//任何值类型转换为System.ValueType类(所有的struct类均派生自System.ValueType) ValueType valueType1 = 1;//int装箱为ValueType ValueType valueType2 = false;//bool装箱为ValueType
IFormattable formattable = 1;//任何非空的值类型转换为它们所实现的接口 int? nullable_i = 1; IComparable comparable = nullable_i;//任何可空的值类型转换为它非空值类型所实现的接口 Enum enum1 = DayOfWeek.Monday;//任何的枚举类型转换为System.Enum DayOfWeek? dayOfWeek = DayOfWeek.Monday; Enum enum2 = dayOfWeek;//任何可空的枚举类型转换为System.Enum //注意,对于可空值类型装箱时: //1、如果可空值类型为null,那么结果是一个空指针引用(null) //2、否则是装箱后的引用对象
9、隐式动态转换
隐式动态转换指的是dynamic类型转换为其他类型,这是一种运行是的转换,如果转换失败,将会抛出异常,如:
object @object = "hello"; dynamic @dynamic = "hello"; string value1 = @object;//编译时就报错 int value2 = @dynamic;//编译时不报错,运行时报错 string value3 = @dynamic;//编译和运行均通过
一般的,如果dynamic对象保存的类型不是所需要的类型,那么它会先将dynamic转换陈给对象,再由对象转换成所需要的类型,也就是说,如果类型S可以通过隐式转换成T,那么S也可以通过隐式动态转换成T,而不需要dynamic对象保存的值类型与所需类型一致
dynamic @dynamic = new ArgumentException[0]; IList<Exception> exceptions = @dynamic;//可以转换
10、隐式常量表达式转换
常量表达式是在编译时计算的表达式,而且非在运行时,例如:
var sum = 1 + 2 + 3; Console.WriteLine("sum is " + sum); var concat = "hello" + " " + "world"; Console.WriteLine(concat);
在编译后,是这样子的:
int num = 6; Console.WriteLine("sum is " + ((int)num).ToString());
string str = "hello world"; Console.WriteLine(str);
可以看到,1+2+3 和 "hello" + " " + "world" 在编译时就被计算,这就是常量表达式。
隐式常量表达式转换指的是下面两种情况下的转换:
1、值类型是int的常量表达式可以隐式的转换为sbyte, byte, short, ushort, uint, ulong,但是要求常量表达式的值在类型允许的范围内 2、值类型是long的常量表达式可以隐式的转换为ulong,但是要求常量表达式的值不能为负数
例如:
byte b = 1 + 1; short s = 2; uint u = 100 * 2; ulong l = 1314L * 520L;
咋一看,这没什么,但是要知道,数值类型默认是int类型,也就是说,上面的byte类型的变量b的值来自于int类型!这就是隐式常量表达式转换的作用。
需要注意的是,这里是常量表达式,而非变量值,且常量表达式的值应该在有效的范围内,否则将会抛出异常,如:
int i = 1; short s = i;//报错,i不是常量表达式 byte b = 1314 + 520;//报错,byte类型值范围在0-255之间 ulong l = 99L - 100L;//报错,ulong类型不能为负数
11、涉及类型参数的隐式转换
这一点其实没什么特别的,一般指的就是泛型参数的转换,其实就是上面隐式转换作用在泛型类型上,参考下面的例子:
public class Demo { //T是struct,所以T可以隐式的转换成ValueType public ValueType GetValueType<T>(T t) where T : struct { return t; } //Exception实现了ISerializable接口,所以T可以隐式的转换成ISerializable public ISerializable GetException<T>(T t) where T: Exception { return t; } }
12、用户定义的隐式转换
C#允许用户可自定义类型或者结构的自定义转换,可以看看:C#自定义转换(implicit 或 explicit)
13、匿名函数转换和方法组转换
匿名函数分为两种:Lambda表达式和匿名方法表达式
Lambda表达式应该都很熟悉,它采用箭头符号来声明主体,而匿名方法表达式采用delegate关键字什么,与普通方法的区别就是没有名称,如:
//Lambda表达式 x => x + 1 x => { return x + 1; } (int x) => x + 1 (int x) => { return x + 1; } (x, y) => x * y () => Console.WriteLine() async (t1,t2) => await t1 + await t2 //匿名方法表达式 delegate (int x) { return x + 1; } delegate { return 1 + 1; }
匿名函数转换就是指Lambda表达式和匿名方法表达式可以隐式的转换成对应的委托,如:
//Lambda表达式转换为委托 Action action1 = () => Console.WriteLine(); Func<int, int> func1 = x => x + 1; //匿名方法表达式转换为委托 Action<int> action2 = delegate (int x) { Console.WriteLine(x); }; //匿名方法表达式中若未指定参数,则表示参数不匹配,可随意 Func<int> func2 = delegate { return 1 + 1; }; Func<int, int> func3 = delegate { return 1 + 1; }; Func<int, int, int> func4 = delegate { return 1 + 1; };
方法组转换指的是,我们可以讲方法隐式的转换成委托,如:
public class Calculator { public int Plus(int a, int b) { return a + b; } public int Minus(int a, int b) { return a - b; } } static void Main(string[] args) { //实例方法 Func<int, int, int> plus = new Calculator().Plus; Func<int, int, int> minus = new Calculator().Minus; //静态方法 Action<string[]> main = Main; //或者 //实例方法 Func<int, int, int> plus = new Func<int, int, int>(new Calculator().Plus); Func<int, int, int> minus = new Func<int, int, int>(new Calculator().Minus); //静态方法 Action<string[]> main = new Action<string[]>(Main); }
参考文档:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions