隐式类型

静态类型和动态类型

静态类型,表达式检查,方法的调用,一切绑定都在编译期间决定好了。动态类型这把这部分操作放到真正的执行期。(代码先被编译成DLL,等到运行程序时再被真正的执行)

C#总体上属于静态类型(C#4引入了动态类型)

显示类型和隐式类型

显式类型即明确的类型,任何变量,字段,方法都需要有一个明确的数据类型

隐式类型 则不必如此

隐式类型的局部变量

以下两种申明方式完全相同

string str = "C#";
var str = "C#";

使用var关键字,必须满足两个条件

  • 变量在申明时就初始化
  • 用于初始化变量的表达式已经具有某一个类型
  • 只能用于局部变量

隐式类型的数组

var array = new[] {1,2,3};

复杂一些,可以是

var array = new[] {"xyz",new object()};

编译器如何进行类型检查呢?

对于数组2来说

  1. 统计每一个元素的类型,获得一个类型数组,对于数组2来说,是string和object
  2. 对类型数组中的每一项进行检查,检查其他项是否可以转换为此项,如果不可以,则排除,对于数组2来说,string可以转为object,反过来不行,所以排除object
  3. 此时类型数组中只剩下了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表达式中使用,诸如:静态字段、实例字段、方法参数、局部变量等

通过生成类来实现变量捕获

  1. 如果表达式中不需要任何变量,那么编译器可以将其变为一个静态方法
  2. 如果表达式中仅需要实例方法,那么编译器也只需要创建一个实例方法
  3. 如果存在局部变量或者参数,那么编译器需要创建一个私有的嵌套类

如果存在以下方法

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;