1. 隐式类型
c#允许使用 var 声明变量,编译期会通过初始化语句右侧的表达式推断出变量的类型。
// i is compiled as an int
var i = 5;
// s is compiled as a string
var s = "Hello";
// a is compiled as int[]
var a = new[] { 0, 1, 2 };
// expr is compiled as IEnumerable<Customer>
// or perhaps IQueryable<Customer>
var expr =
from c in customers
where c.City == "London"
select c;
// anon is compiled as an anonymous type
var anon = new { Name = "Terry", Age = 34 };
// list is compiled as List<int>
var list = new List<int>();
使用var来声明变量,我们可以把注意力放在更为重要的使用部分,只需要准确的变量名,既提供正确的语义,剩下的编译期会去操心变量的类型与后面的使用方法是不是匹配。
甚至有的时候,开发者可能会定义错误或者不合适的数据类型,反而不如var方式声明隐式类型。 如 C# :IQueryable & IEnumerable 中:
var q = from c in dbContext.Customers Where c.City == "London" select c;
var finalAnswer = from c in q order by c.Name select c;
上面的 q 编译期会通过表达式,将 q 退段位 IQueryable 类型,从而将 Where 表达式与第二行的 order by 进行表达树组合,在一次sql查询操作里完成。
假如开发者明确声明了 q 的类型,但是声明为 IEnumerable,那么在第一行结束,就会把 Where 查询到的所有数据传到本地,之后再进行排序。
IEnumerable<Customer> q = from c in dbContext.Customers Where c.City == "London" select c;
var finalAnswer = from c in q order by c.Name select c;
隐式类型的数据转换
在使用隐式类型的时候,需要注意的是类型转换的问题。
有些转换是宽化转化(widening conversion),有些则是窄化转换(narrowing conversion),窄化转换会导致精度下降,例如从 long 到 int 的转换。
public class VarTypeConvert
{
public static void Run()
{
IMagicNumberGenerator<double> doubleMNGenerator = new DoubleMNGenerator();
IMagicNumberGenerator<float> floatMNGenerator = new FloatMNGenerator();
IMagicNumberGenerator<int> intMNGenerator = new IntMNGenerator();
var d = doubleMNGenerator.GenerateMagicNumber();
var dtotal = 100 * d / 6;
Console.WriteLine($"Double magic number: {d}, Total: {dtotal}");
var f = floatMNGenerator.GenerateMagicNumber();
var ftotal = 100 * f / 6;
Console.WriteLine($"Float magic number: {f}, Total: {ftotal}");
var i = intMNGenerator.GenerateMagicNumber();
var itotal = 100 * i / 6;
Console.WriteLine($"Int magic number: {i}, Total: {itotal}");
}
}
-----------------Output--------------------------
Double magic number: 1, Total: 16.666666666666668
Float magic number: 1, Total: 16.666666
Int magic number: 1, Total: 16
都是计算 100 * x / 6,但是由于 x 的类型不同,隐式类型的公式结果大不相同。由于代码采用了隐式类型的局部变量,因此编译期自己推断变量的类型,即根据赋值符号右边的部分做出最佳选择。
即使我们将公式结果定义为 double,由于 MagicNumbe 是int类型,程序仍然会按照整数运算规则进行计算:
var i = intMNGenerator.GenerateMagicNumber();
double dtotal = 100 * i / 6;
Console.WriteLine($"Int magic number: {i}, Total: {dtotal}");
// Output: Int magic number: 1, Total: 16
2. 匿名类型
隐式类型的局部变量是为了支持匿名类型机制而加入 c# 语言的。如下匿名类型:
var v = new { Amount = 108, Message = "Hello" };
Console.WriteLine(v.Amount + v.Message);
我们在使用查询表达式的时候,常会使用匿名类型,从而对每个对象的属性子集处理和返回:
var productQuery =
from prod in products
select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
匿名类型为我们提供了一种将只读属性封装到单个对象,又不需要显示先定义类型的便捷方法。类名由编译期生成,每个属性的类型由编译期推断。
匿名类型支持采用 with 表达式形式的非破坏性修改,从而使开发者可以创造匿名类型的新实例。
var p1 = new NamedPoint("A", 0, 0);
Console.WriteLine($"{nameof(p1)}: {p1}"); // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }
var p2 = p1 with { Name = "B", X = 5 };
Console.WriteLine($"{nameof(p2)}: {p2}"); // output: p2: NamedPoint { Name = B, X = 5, Y = 0 }
值得注意的是,匿名类的 Equals 和 GetHashCode 方法是根据属性的 Equals 和 GetHashCode 定义的,因此仅当同一匿名类型两个实例所有属性都相等,这两个实例相等。
3. 编译时类型 和 运行时类型
编译时类型即直接声明的类型,或推断类型(c# 隐式类型)。
需要注意的是隐式类型的变量,编译器推断出来的类型不一定是我们期望的类型,比如:
// 推断类型为字符串
var message1 = "This is a string of characters";
// 期望类型为 字符的枚举数组
IEnumerable<char> message2 = "This is a string of characters";
运行时类型则是在代码运行阶段实际使用的类型。编译时类型确定编译期执行的所有操作,包括方法调用解析、重载决策以及可用的隐式显示强制转换。运行时类型确定在运行时解析度所有操作,包括调度虚拟方法、计算 is 和 switch 表达式以及其他类型测试API。
object o = Factory.GetObject();
MyType t = o as MyType;
// as 在运行时判断是否是 MyType,如果是则进行类型转换,否则返回 null
if (t != null) ...
else ...
再来看一个编译时类型和运行时类型在类型转换时的例子:
namespace LearnCSharp.Type
{
public class MyType
{
public int data { get; set; }
}
public class SecondType
{
private MyType _value;
public SecondType()
{
_value = new MyType()
{
data = 10
};
}
// 定义类型转换从 SecondType -> MyType
public static implicit operator MyType(SecondType st)
{
return st._value;
}
}
public class LTypeFactory
{
public static SecondType GetObject()
{
return new SecondType();
}
}
// 测试类型转换
public class Program
{
public static void Main(string[] args)
{
SecondType secondType = new SecondType();
// 根据SecondType 中的声明,可以成功转换类型
MyType myType = secondType;
Console.WriteLine($"Convert SecondType to MyType success: Data = {myType.data}");
object o = LTypeFactory.GetObject();
MyType t = o as MyType; // 由于运行时类型并不是 MyType,as运算符只会判断运行期类型,不会执行强制类型转换
if (t != null)
{
Console.WriteLine($"Convert object to MyType success: Data = {t.data}");
}
else
{
Console.WriteLine("Convert object to MyType failed");
}
try
{
MyType t1;
t1 = (MyType) o;
Console.WriteLine($"Convert object to MyType success: Data = {t1.data}");
}
catch (Exception ex) // 强制类型转换以对象的编译期类型为依据,所以会认为 o 是 object,而非 SecondType,从而强制转换失败
{
Console.WriteLine($"Convert object to MyType failed: {ex.Message}");
}
VarTypeConvert.Run();
}
}
}
--------------------Output----------------------------------
Convert SecondType to MyType success: Data = 10
Convert object to MyType failed
Exception thrown: 'System.InvalidCastException' in LearnCSharp.dll
Convert object to MyType failed: Unable to cast object of type 'LearnCSharpType.SecondType' to type 'LearnCSharp.Type.MyType'.
4. 装箱和取消装箱操作
装箱是将值类型转换为object类型,或由此值类型实现的任何接口类型的过程,新创建的引用对象相当于一个箱子,分配在堆上面,其中含有原值的一份拷贝。
取消装箱则是从对象中提取值类型。会把已经装箱的那个值拷贝一份出来。
相对于简单的赋值而言,装箱和取消装箱过程需要大量的计算,装箱时,需要分配构造新对象。取消装箱所需要的强制转换也需要大量计算。
int i = 123;
object o = i; // explicit boxing
int i = 123; // a value type
object o = i; // boxing
int j = (int)o; // unboxing