C#中的隐式转换
你是否考虑过这个问题:为什么不同类型之间的变量可以赋值,⽽不需要强制转换类型?如:
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>)。
隐式可空类型转换就是指⼀个值类型可以隐式的转换为它的可空类型,如:
writeline输出数值变量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#允许⽤户可⾃定义类型或者结构的⾃定义转换,可以看看:
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 { return1 + 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 { return1 + 1; };
Func<int, int> func3 = delegate { return1 + 1; };
Func<int, int, int> func4 = delegate { return1 + 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);
}
参考⽂档:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论