深入理解C#|03 C#3、LINQ以及相关特性
隐式类型
静态类型和动态类型
静态类型,表达式检查,方法的调用,一切绑定都在编译期间决定好了。动态类型这把这部分操作放到真正的执行期。(代码先被编译成DLL,等到运行程序时再被真正的执行)
C#总体上属于静态类型(C#4引入了动态类型)
显示类型和隐式类型
显式类型即明确的类型,任何变量,字段,方法都需要有一个明确的数据类型
隐式类型 则不必如此
隐式类型的局部变量
以下两种申明方式完全相同
string str = "C#";
var str = "C#";
使用var关键字,必须满足两个条件
- 变量在申明时就初始化
- 用于初始化变量的表达式已经具有某一个类型
- 只能用于局部变量
隐式类型的数组
var array = new[] {1,2,3};
复杂一些,可以是
var array = new[] {"xyz",new object()};
编译器如何进行类型检查呢?
对于数组2来说
- 统计每一个元素的类型,获得一个类型数组,对于数组2来说,是string和object
- 对类型数组中的每一项进行检查,检查其他项是否可以转换为此项,如果不可以,则排除,对于数组2来说,string可以转为object,反过来不行,所以排除object
- 此时类型数组中只剩下了string,故此类型为string,如果此时还剩下多个类型,那么编译报错
匿名类型
基本语法和行为
创建一个匿名类型
var player = new
{
Name = "Test",
Score = 3500
};
我们无须定义一个Player类,便可以进行上述操作,此时的player就是一个匿名类型
投射初始化
假设有这样一个类
public class Player
{
public string Name;
public float Score;
public Player(string Name,float Score){...}
}
那么有
var player = new Player("Test",3500);
var test = new
{
player.Name,
TestScore = player.Score
}
以上初始化方式等同于
var test = new
{
Name = player.Name,
TestScore = player.Score
}
关键在于,当匿名对象的目标属性,与源属性名称相同时,可以直接进行替换
lambda表达式
基本语法:参数列表 => 表达式主体
Action<string> action = (string msg) =>
{
Console.WriteLine(msg);
};
如果主体只有一条表达式,则可以继续简化
Action<string> action = (string msg) => Console.WriteLine(msg);
如果参数列表也可以推断,那同样可以省略
Action<string> action = msg => Console.WriteLine(msg);
变量捕获
使用lambda表达式时,编译器需要把lambda表达式转换成一个方法
任何可以在普通方法中使用的变量,都可以在lambda表达式中使用,诸如:静态字段、实例字段、方法参数、局部变量等
通过生成类来实现变量捕获
- 如果表达式中不需要任何变量,那么编译器可以将其变为一个静态方法
- 如果表达式中仅需要实例方法,那么编译器也只需要创建一个实例方法
- 如果存在局部变量或者参数,那么编译器需要创建一个私有的嵌套类
如果存在以下方法
public class TestClass
{
public Action<string> CreateAction(string mehtodParam)
{
var methodLocal = "methodLocal";
Action<string> action = (lambdaParam) =>
{
var lambdaLocal = "lambdaLocal";
Console.WriteLine(methodParam);
Console.WriteLine(methodLocal);
Console.WriteLine(lambdaLocal);
}
methodLocal = "xxx";
return action;
}
}
则编译器需要创建一个如下的私有嵌套类来存放信息
public class TestClass
{
private class LambdaContext
{
public TestClass testClass; // 保存对于外部类的引用
public string methodParam; // 捕获变量1:方法的参数
public string methodLocal; // 捕获变量2:方法的局部变量
public void Method(string lambdaParam)
{
var lambdaLocal = "lambdaLocal";
Console.WriteLine(methodParam);
Console.WriteLine(methodLocal);
Console.WriteLine(lambdaLocal);
}
}
}
具体使用时,则会变为
public class TestClass
{
public Action<string> CreateAction(string mehtodParam)
{
var context = new LambdaContext();
context.textClass = this;
context.methodParam = methodParam;
context.mehtodLocal = "methodLocal";
Action<string> action = context.Method;
context.methodLocal = "xxx"; // 注意这里,这里改变的内嵌类中的值
return action;
}
}
强调一下,lambda捕获的均是变量本身,而非副本,任何对于变量的修改都会影响表达式
局部变量的多次实例化
如果有以下代码
public class TestClass
{
public List<Action> CreateAction()
{
var actions = new List<Action>();
for(int i=0 ; i<5 ; i++)
{
var text = i.ToString();
actions.add(()=>Console.WriteLine(text));
}
return actions;
}
}
关在在于,每一次循环中,都初始化了一个text变量,这就存在5个不同的text分别被不同的lambda捕获
编译器会转译成如下形式
public class TestClass
{
private class LambdaContext
{
public string text;
public void Method()
{
Console.WriteLine(text);
}
}
public List<Action> CreateAction()
{
var actions = new List<Action>();
for(int i=0 ; i<5 ; i++)
{
var context = new LambdaContext(); // 每一次循环都有一个新的上下文,这很关键
context.text = i.ToString();
actions.add(context.Method)
}
return actions;
}
}
多个作用域下的变量捕获
如果有以下代码
public class TestClass
{
public List<Action> CreateAction()
{
var actions = new List<Action>();
int outerParam = 0;
for(int i=0; i<2; i++)
{
int innerParam = 0;
Action action = ()=>
{
Console.WriteLine(outerParam,innterParam);
outerParam++;
innterParam++;
}
actions.add(action);
}
return actions;
}
}
此时如果我们依次执行actions数组两遍
actions[0]?.invoke(); // 1,0
actions[0]?.invoke(); // 2,1
actions[1]?.invoke(); // 3,0
actions[1]?.invoke(); // 4,1
编译器的转译结果如下,显而易见的,外部变量被两个委托共同持有,而内部变量则是独立的
public class TestClass
{
private class OuterClass
{
public int outerParam;
}
private class InnerClass
{
public OuterClass outerClass;
public int innerParam;
public void Method()
{
Console.WriteLine(outerClass.outerParam,innterParam);
outerClass.outerParam++;
innterParam++;
}
}
public List<Action> CreateAction()
{
var actions = new List<Action>();
var outerClass = new OuterClass();
outerClass.outerParam = 0;
for(int i=0; i<2; i++)
{
var innerClass = new InnerClass();
innerClass.outerParam = outerClass;
innerClass.innerParam = 0;
actions.add(innerClass.Method);
}
return actions;
}
}
表达式树
表达式树是将代码按数据来表示的一种方式,有点类似对于lambda表达式的反射信息
一般来说不会主动构建,都是让编译器负责转换的
主动创建一个表达式树
Expression<Func<int,int,int>> adder = (x,y) => x+y;
Console.WriteLine(adder); // (x, y) => (x + y)
看这个输出结果,是不是非常类似于typeof(int)这样的东西…
Expression<TDelegate>是处理TDelegate类型的表达式树类型,TDelegate必须是委托类型
将表达式树编译成委托
很简单,只需要
Expression<Func<int,int,int>> adder = (x,y) => x+y;
Func<int,int,int> func = adder.Compile(); // 此时与普通的委托没有区别
一般来说会配合反射进行使用
扩展方法
语法
如果有这样一个类
public class Test
{
public string name;
}
如果我们想打印name,那么可以
public class Test
{
public void Log()
{
Console.WriteLine(name);
}
}
或者,可以设计一个扩展方法
public statis class Extensions
{
public static void Log(this Test test)
{
Console.WriteLine(test.name);
}
}
上述二者完全等价,均可以通过test.Log()进行调用
区别在于,如果Log是一个实例方法,那么当test为null时,调用会报错
而如果Log是一个扩展方法,那么即使test为null,仍然可以调用方法,会把null作为第一个参数传入
链式调用
string[] words = {"keys","coat","laptop","bottle"};
var query = words
.Where(word => word.Length > 4) // 筛选
.OrderBy(word => word) // 排序
.Select(word => word.ToUpper()); // 转换
foreach (var word in query)
{
Console.WriteLine(word);
}
查询语句
标准示例
string[] words = {"keys","coat","laptop","bottle"};
var query = from word in words
where word.Length > 4
orderby word
select word.ToUpper();
范围变量与隐性标识符
// 不使用范围变量
var query = words.Where(word => word.Length > 4);
// 使用范围变量
var query = from word in words where word.Length > 4;
可以发现,范围变量充当了语句的输入
也可以使用let子句引入新的范围变量
var query = from word in words
let length = word.Length
where Length > 4;