Siki_Unity_2-9_C#高级教程(未完)

Unity 2-9 C#高级教程

任务1:字符串和正则表达式
任务1-1&1-2:字符串类string

System.String类(string为别名)

注:string创建的字符串是不可变的,一旦进行了初始化,就不能改变其内容了

string的声明:string s = "....";
字符串长度:int length = s.Length;
比较string大小:直接使用 == 即可:(s == "xxx")
字符串的连接:直接使用 + 即可:s = "..." + s;
  这里s的修改不是直接修改其内容,而是重新赋值,耗费性能
  -- 新建字符串用来存储连接后的字符串,并使引用s指向这个新字符串,旧的进入GC
字符串中按索引取得字符:s[index];
遍历字符串:for循环

常用方法:

CompareTo() -- 比较字符串内容,各个字符依次比较
  若两个字符串相等则返回0,若当前字符串在字母表排序比较靠前则返回-1,否则返回1
  s.CompareTo(s1); // 返回值为int,0表示相等,-1表示s靠前,1表示s靠后
  应用:如人名的排序等

Replace() -- 用另一个字符或者字符串替换字符串中给定的字符或者字符串
  string newS = s.Replace('a', 'b'); // replace 'a' with 'b'
  string newS = s.Replace("a", "bbbb");

Split() -- 在出现给定字符的地方,把字符串拆分称一个字符串数组
  string[] strArray = s.Split(','); // 通过','将字符串分割

SubString() -- 在字符串中检索给定位置的子字符串
  "www.devsiki.com".SubString(4, 7);
    // 从index=4之后的位置开始截取,长度为7的子字符串,devsiki
    若不给定长度,则会一直取到原字符串结尾

ToLower()/ ToUpper() 把字符串转换成小写/大写形式

Trim() -- 删除首尾的空白
  string newS = "  adfa ad a  ".Trim();  // "adfa ad a"
  应用:如玩家在输入用户名的时候,首尾是不能有空格的
     对于中间的空格而言,可以使用Replace(" ", "");

IndexOf() -- 取得字符串第一次出现某个给定字符串或者字符的位置
  int index = s.IndexOf("dev"); // 返回第一个字符对应的index值,若不存在,则返回-1

Concat() -- 合并字符串

CopyTo() -- 把字符串中指定的字符复制到一个数组中

Format() -- 格式化字符串

Insert() -- 把一个字符串实例插入到另一个字符串实例的制定索引处

Join() -- 合并字符串数组,创建一个新字符串

任务1-3&1-4&1-5:StringBuilder类

Sysytem.Text.StringBuilder

StringBuilder类比string的效率更高:
  比如StringBuilder.Append("xxx"); 和string之间的+号连接操作的比较:
    string: 每次修改都需要开辟另外的存储空间
    StringBuilder: 若当前空间足够,直接修改即可

创建:
  StringBuilder sb = new StringBuilder("...");  -- 将一个string传递给构造函数
  StringBuilder sb = new StringBuilder(20); -- 初始长度
  StringBuilder sb = new StringBuilder("...", 100); -- 字符数量小于100时就不需申请内存
    如果超过100个字符时,会重新申请一个200(2倍)的内存区域并赋值,删除原来的
    一般来说,预估sb可能的大小,在进行初始化的时候申请该大小的内存区域

方法:

Append(string) -- 在字符串末尾追加一个字符串
  sb.Append("xxx");
  -- s = s + "xxx";

Insert(index, string) -- 从特定index开始插入字符串
  sb.Insert(0, "http://");

Remove(startIndex, length) -- 从当前字符串中删除指定长度的字符串
  sb.Remove(0, 3); // 删除前三个字符

Replace() -- 用某字符/字符串替换另一个字符/字符串
  sb.Replace(".", ""); 
  注意:sb.Replace('.', ''); 是不行的,不能替换成空字符,但是可以替换成空字符串

ToString() -- 将stringBuilder中存储的字符串,提取成一个(不可变的)string

任务1-6:VS插件Resharper

安装:VS中,工具 -> 扩展和更新 -> 联机 -> 搜索resharper -> 选择ReSharper(图标c#)

任务1-7:正则表达式及其方法

正则表达式 Regular Expression: 表述一个字符串的书写规则

用途:
  1. 检索:通过正则表达式,从字符串中获取我们想要的部分
  2. 匹配:判断给定的字符串是否符合正则表达式的过滤逻辑
  等等:验证、提取、分割、替换等

正则表达式由元字符(普通字符和特殊字符)组成
  (元字符在下一任务中详述)

常用的判断正则表达式的c#方法:

System.Text.RegularExpressions下的Regex类

IsMatch() -- 返回bool判断字符串是否匹配正则表达式
bool IsMatch(string input, string pattern);
  参数: input: 要搜索匹配项的字符串。
  pattern: 要匹配的正则表达式模式。
  返回结果: 如果正则表达式找到匹配项,则为 true;否则,为 false。
bool IsMatch(string input, string pattern, RegexOptions options);
  options: 枚举值的一个按位组合,这些枚举值提供匹配选项。
  返回结果: 如果正则表达式找到匹配项,则为 true;否则,为 false。
bool IsMatch(input, pattern, RegexOptions options, TimeSpan matchTimeout);
  matchTimeout: 超时间隔,或System.Text.RegularExpressions.   
                Regex.InfiniteMatchTimeout指示该方法不应超时。
  返回结果: 如果正则表达式找到匹配项,则为 true;否则,为 false。

Match() -- 在字符串中搜索指定的正则表达式的第一个匹配项。
  返回一个包含有关匹配的信息的对象。
Match Match(string input, string pattern);
Match Match(string input, string pattern, RegexOptions options);
Match Match(input, pattern, RegexOptions options, TimeSpan matchTimeout);

Matches() -- 与Match()不同的是,返回的是MatchCollection,包含所有的匹配项
  重载方法的参数与Match()完全相同

Replace() -- 将匹配正则表达式的所有地方用新的字符串替换
Replace(string input, string pattern, string replacement)
  input是源字符串,pattern是匹配的条件,replacement是替换的内容,
  就是把符合匹配条件pattern的内容转换成replacement
  如:string result = Regex.Replace("abc", "ab", "##");
    //结果是##c,就是把字符串abc中的ab替换成##
Replace(input, pattern, replacement, RegexOptions options)
  RegexOptions是一个枚举类型,用来做一些设定.
  // 比如:如果在匹配时忽略大小写就可以用RegexOptions.IgnoreCase
Replace(input, pattern, MatchEvaluator evaluator);
  evaluator是一个代理,简单而言是一个函数指针,把一个函数做为参数参进来
  由于C#里没有指针就用代理来实现类似的功能。
  可以用代理绑定的函数来指定你要实现的复杂替换.
Replace(input, pattern, MatchEvaluator evaluator, RegexOptions options);

关于Regex.Options:

Split() -- 在正则表达式匹配的位置,将文本拆分为一个字符串数组,并返回
string[] Split(string input, string pattern); 
string[] Split(string input, string pattern, RegexOptions options); 
string[] Split(input, pattern, RegexOptions options, TimeSpan matchTimeout);

@符号:避免编译器去解析字符串中的转义字符,而作为正则表达式的语法(元字符)存在
  如:string s = @"www.baidu.com lkjsdflkj";  -- 这里的 就是 ,没有其他意义

任务1-8~1-13:特殊元字符 (定位元字符、基本语法元字符、反义字符、重复描述字符、择一匹配符、分组操作符)

定位元字符:

字符    说明 
   匹配单词的开始或结束 
B   匹配非单词的开始或结束 
^     匹配必须出现在字符串的开头或行的开头 
  string str = "I am Blue cat"; 
  Console.WriteLine(Regex.Replace(str, "^","开始:")); // "开始:I am Blue cat"
$   匹配必须出现在以下位置:字符串结尾、字符串结尾处的
    之前或行的结尾。 
A   指定匹配必须出现在字符串的开头(忽略 Multiline 选项)。 
z   指定匹配必须出现在字符串的结尾(忽略 Multiline 选项)。 
z   指定匹配必须出现在字符串的结尾或字符串结尾处的 之前 (忽略 Multiline 选项) 
G   指定匹配必须出现在上一个匹配结束的地方。
     与 Match.NextMatch() 一起使用时,此断言确保所有匹配都是连续的。

基本语法元字符:

字符 说明 
.     匹配除换行符以外的任意字符 
w  匹配字母、数字、下划线、汉字 (指大小写字母、0-9的数字、下划线_) 
W  w的补集 ( 除“大小写字母、0-9的数字、下划线_”之外) 
s   匹配任意空白符 (包括换行符/n、回车符/r、制表符/t、垂直制表符/v、换页符/f) 
S   s的补集 (除s定义的字符之外) 
d   匹配数字 (0-9数字) 
D   表示d的补集 (除0-9数字之外)

实例:
  string input = Console.ReadLine();
  string pattern = @"^d*$";  // 正则表达式: 全数字--因为d不是转义字符,需要@
  if(Regex.IsMatch(input, pattern)) {
    Console.WriteLine("Valid");
  }

反义字符:

字符  说明
W
S

B     匹配不是单词开头或结束的位置
[^x]    匹配除了x以外的任意字符 
[^adwz] 匹配除了adwz这几个字符以外的任意字符

中括号:
[ab]   匹配中括号中的字符
[a-c]  a字符到c字符之间的字符 (a/b/c)

实例:查找除了ahou以外的所有字符
  string strFind1 = "I am a Cat!", strFind2 = "My Name's Blue cat!";
  Regex.Replace(strFind1/2, @"[^ahou]","*"));
  // strFind1->**a**a**a*
  // strFind2->****a*******u***a**

重复描述字符:

字符  说明
{n}   匹配前面的字符 =n次
{n,}    匹配前面的字符 >=n次
{n,m}   匹配前面的字符 n~m次
?    重复零次或一次 =0/1
+    重复一次或更多次 >=1
*    重复零次或更多次 >=0

实例:校验输入内容是否为合法QQ号(备注:QQ号为5-12位数字)
  string regexQq = @"^d{5,12}$";

择一匹配符:

字符  说明
|    将两个匹配条件进行逻辑“或”(Or)运算。

实例:
  1. 查找数字或字母
    string str = "ad(d3)-df";
    string regexPattern = @"[a-z]|d";
    MatchCollection newStr = Regex.Matches(str, regexPattern);
    可用foreach(Match char in newStr) 来遍历得到的结果
  2. 示例二:将人名输出("zhangsan;lisi,wangwu.zhaoliu")
    string strSplit = "zhangsan;lisi,wangwu.zhaoliu";
    string regexSplitstr = @"[;] | [,] | [.]";
    // 使用string regexSplitstr = @"[;,.]"; 也可以
    string[] resArray = Regex.Split(strSplit, regexSplitstr);
分组操作符:

用小括号来指定子表达式(也叫做分组)

实例:校验IP4地址 (如: 192.168.1.4, 为四段, 每段最多三位, 每段为0~255,且第一位不为0) string regex = @"^(((2[0-4]d|25[0-5]|[01]?dd?).){3}(2[0-4]d|25[0-5]|[01]?dd?))$";
Regex.IsMatch(inputStrIp4, regexStrIp4));

任务2:委托、Lambda表达式和事件
任务2-1&2-2:什么是委托

如果要把方法当做参数来传递的时候,就要通过委托
简单而言:委托是一个类型,可以赋值一个方法的引用给该类型
-- 之前的变量都是赋值数据的

委托的声明:
  使用一个类的两个阶段:
    定义类:告诉编译器这个类由什么字段和方法组成
    定义后可以使用对象:用这个类去实例化对象

  使用一个委托的两个阶段:
    定义委托:告诉编译器这个委托可以指向哪些类型的方法
    创建该委托的实例:

  定义委托的语法:
    delegate void IntMethodInvoker(int x);
    定义了名为IntMethodInvoker的委托,指向的方法带有一个int参数,方法返回void

  创建委托的实例/ 使用委托: 

第一种创建委托的方式:构造函数
第一种使用委托的方式:通过委托名

private delegate string GetAString(); // 定义委托,指向的方法没有参数,返回string
static void Main() {
    int x = 40;
    // 创建一个委托实例,指向对象x的ToString()方法,没有写()
    // 注:此时没有调用该方法,只是将委托stringMethod指向了x的ToString()方法
    GetAString stringMethod = new GetAString(x.ToString);
    // 通过委托实例stringMethod来调用指向的方法
    Console.WriteLine(stringMethod());
    // 通过委托调用的方法,和直接调用方法的结果是一样的
}

第二种创建委托的方式:直接通过函数名
  GetAString stringMethod = x.ToString;
第二种使用委托的方式:通过Invoke方法从而调用委托指向的方法
  stringMethod.Invoke();

把委托类型的实例当做参数来使用:

private delegate void PrintString();

PrintString method = Method1;
PrintStr(method);
method = Method2;
PrintStr(method); // 输出 "Method1
Method2"

static void PrintStr(PrintString print) {
    print(); // 将PrintString类型的委托作为参数传入PrintStr方法
}

static void Method1() {
    Console.WriteLine("Method1");
}
static void Method2() {
    Console.WriteLine("Method2");
}

任务2-3:Action委托

除了上一节提到的自定义的委托类型,系统内置/预定义的委托类型:Action和Func

Action委托指向的是返回值为void,且没有参数的方法
  Action a = MethodName;

Action委托的扩展<泛型>:可以指向返回值是void,而且有参数的方法 (最大支持16个参数)
  -- 参数列表必须和指向的方法的参数列表顺序对应
  Action<int> a = MethodName;  // 有一个int参数
  Action<int, bool> a = MethodName; // 有一个int和一个bool参数
  -- 指向重载方法时,系统会自动匹配参数合适的方法

任务2-4:Func委托

Func委托指向的是有返回值,且没有参数的方法
  Func<int> f = MethodName; // 必须有一个泛型,表示返回值类型

Func委托的扩展:可以指向有返回值,且有参数的方法 (最大支持16个参数)
  Func<string, int> f = MethodName; // 参数为string, 返回值为int
  -- 最后一个是返回类型,其他的都是参数类型

任务2-5&2-6:通用类型的方法 -- 实例:冒泡排序

冒泡排序:从小到大
  第一轮开始:
    0号与1号比较,若0号大,则0号和1号交换位置;
    1号与2号比较,若1号大,则1号和2号交换位置;
    以此类推,一轮过后,最大的数字被交换到数组的最后;
  第二轮开始:
    0号与1号比,一直比到n-2号与n-1号比
    一轮过后,次大的数字被交换到数组的n-1处
  n轮过后,数组有序

改进:
  当进行一轮比较之后,没有数字发生位置交换,则判定已经为有序数组,终止循环

int类型的冒泡排序:

bool swapped = true;
do{
    swapped = false;
    for(int i =0;i<sortArray.Length -1;i++){
        if(sortArray[i]>sortArray[i+1]){
            int temp= sortArray[i];
            sortArray[i]=sortArray[i+1];
            sortArray[i+1]=temp;
            swapped = true;
        }
    }
}while(swapped);

扩展-->通用的冒泡排序:通过泛型+委托的方式实现

实例:对雇员类Employee,按照Salary进行排序

class Employee{
    public Employ(string name,decimal salary){
        this.Name = name;
        this.Salary = salary;
    }
    public string Name{get;private set;}
    public decimal Salary{get;private set;}
    public static bool CompareSalary(Employee e1,Employee e2){
        return e1.salary>e2.salary;
    }
}

通过泛型定义排序方法:
  // 1. 通过委托的方式将比较函数传递过来
  // 2. 因为不同类的比较方法是不同的,故针对每个类写出对应的比较大小方法即可
    见Employee.CompareSalary ()
  // 3. 调用该方法的时候通过comparison委托将对应类的比较方法传入即可
  public static void Sort<T> (List<T> sortArray, Func<T, T, bool> comparison)

public static void Sort<T>(T[] sortArray,Func<T,T,bool> comparision ){
    bool swapped = true;
    do{
        swapped = false;
        for(int i=0;i<sortArray.Count-1;i++){
          if(comparision(sortArray[i+1],sortArray[i])){
              T temp = sortArray[i];
              sortArray[i]=sortArray[i+1];
              sortArray[i+1]=temp;
              swapped = true;
          }
        }
    }while(swapped);
}

使用:

static void Main(){
    Employee[] employees = {
        new Employee("Bunny",20000),        
    new Employee("Bunny",10000),        
    new Employee("Bunny",25000),            
    };
    Sort<Employee>(employees, Employee.CompareSalary);
}

任务2-7:多播委托

多播委托:指向了多个方法的委托

用处:使用多播委托,可以按照顺序调用多个方法,但是只能得到最后一个方法的结果

一般把多播委托的返回类型声明为void

多播委托包含一个逐个调用的委托集合,若其中一个抛出异常,整个迭代就会停止

如何进行多播委托:
  Action action = Test1;
  action += Test2;  // 添加一个委托的引用给action
  // 此时action() 会顺序执行Test1()和Test2()
  action -= Test1;
  // 删除引用,可以直接删除
  action -= Test2;
  // 报错 -- 当一个委托没有指向任何方法时,会出现空指针异常

如何取得多播委托中所有的委托:
  Delegate[] delegates = action.GetInvocationList();
  foreach(Delegate d in delegates) {
    d.DynamicInvoke(null); // 单独调用 -- 如果需要参数,则需传递
  }

任务2-8:匿名方法

之前使用委托都需要先定义一个方法,然后将方法指定给委托的实例。

匿名方法:
  定义一个没有方法名的方法 -- 本质就是方法,只不过没有定义名字
  用delegate关键字
  不用声明返回类型,直接在方法中返回即可

Func<int,int,int> plus = delegate (int a,int b){
    return a + b;
};
int res = plus(34,34);
Console.WriteLine(res);

匿名方法无法直接调用,只能通过委托进行调用

一般来说,匿名方法用于进行回调

任务2-9:Lambda表达式

Lambda表达式:匿名方法的简写形式

Lambda表达式的书写规则:
  1. 不用写delegate
  2. 不用写参数类型,因为在委托中已经定义了参数类型
  3. 使用 => 表示这是一个Lambda表达式

实例:

Func<int,int,int> plus = delegate (int a,int b){
    return a + b;
};
--> Lambda
Func<int,int,int> plus = (a,b)=> { 
    return a + b; 
};

=>的左边列出了参数(只有一个参数的时候可以不写括号);
=>的右边为匿名方法的方法体(只有一个语句时可以不写大括号;
  且需要return的时候可以不写return关键字)
  如:Func<int int> f = a => a+1;  // 因为有返回值int,所以传入参数a,返回a+1

Lambda表达式外部的变量:
  通过Lambda表达式可以访问外部的变量
  如:int somVal = 5;
    Func<int, int> f = x => x + somVal;
  这个时候,如果没有正确使用,会变得非常危险:
    因为一个方法的使用一般是通过传递的参数决定的
    而由于可以访问外部变量,导致执行也会被外部变量的变化而影响,结果不可控

任务2-10:事件 

之前学了委托:
  实例:

class Program {
    public delegate void MyDelegate(); // 定义委托类型
    public MyDelegate myDelegate; // 声明委托变量,作为成员变量
    
   static void Main(string[] args) {
        Program program;
        program.myDelegate = Test(); // 委托指向方法
        myDelegate(); // 调用委托指向的方法
    }

    static void Test() {
        ...
    }
}

事件(event):具有特殊签名的委托,是类/对象向其他类/对象通知发生的事情的一种委托
  
事件基于委托,为委托提供了一个发布/订阅机制

实例 -- 在上例中修改
  在声明委托变量的时候,加上event关键字
    public event MyDelegate myDelegate; -- 这就是一个事件了

事件的性质:
  1. 委托可以声明成一个局部变量,但是事件只能作为一个类的成员变量
  2. 事件的命名一般为NameEvent
  3. 事件的返回值是一个委托类型

任务2-11&2-12:观察者设计模式 && 委托和事件的区别

观察者设计模式:
  有观察者和被观察者,当被观察者做出某个操作时,触发事件,所有观察者做出对应操作

例: Unity中,点击开始按钮 (被观察者),很多方法如加载场景打开音乐 (观察者)就随之调用

实例:猫与老鼠
  有三只动物,猫(名叫Tom),还有两只老鼠(Jerry和Jack),当猫叫的时候,触发事件(CatShout),然后两只老鼠开始逃跑(MouseRun)

猫类:

class Cat {
    private string name;
    public Cat(string name) {
        this.name = name;
    }
    public void CatShout() {
        // 被观察者的状态改变
        Console.WriteLine(name + " mew);
    }
}

鼠类:

class Mouse {
    private string name;
    public Mouse(string name) {
        this.name = name;
    }
    public void RunAway() {
        Console.WriteLine(name + " run away");
    }
}

Main():

class Program {
    public static void Main(string[] args) {
        Cat cat = new Cat("Tom");
        Mouse mouse1 = new Mouse("Jerry");
        Mouse mouse2 = new Mouse("Jack");
        cat.CatShout();
    }
}

这个时候Cat.CatShout()并不能将消息广播给Mouse
需要手动修改,在该函数中传递两个Mouse,并调用mouse.RunAway()
-- 无扩展性,若添加了其他老鼠,需要修改Cat的代码 -- 耦合性高

解决方法:优化1 -- 通过委托
  在Cat中定义一个委托,让观察者将自己的对应操作添加到委托中
  在CatShout()中调用这个委托即可

在Cat中:

public Action CatShouting;

// 在CatShout()中调用这个委托
public void CatShout() {
    if(CatShouting!=null) { // 安全判断
        CatShouting();
    }
}    

在Main中:
  cat.CatShouting += mouse1.RunAway; // 进行注册

优点:
  如果有新的老鼠,直接在Main中注册即可,不需要改写Cat类

缺点:
  每次新建观察者时,都需要进行注册
  因为每一个观察者都需要进行注册

解决方法:优化2 -- 将猫的对象传给Mouse的构造函数,在构造函数中进行注册

在Mouse的构造函数中传入一个猫的对象
  public Mouse(string name, Cat cat) {
    cat.CatShouting += this.RunAway;

缺点:但是委托CatShouting在外界可以直接被调用
  如在Main中调用 cat.CatShouting();
  这就比较危险了
  因为Cat自身的状态改变cat自己知道就好,不应该通过外界调用

解决方法:优化3 -- 通过事件

在Action的声明中加上event:
  public event Action CatShoutingEvent; -- 命名规则+event

此时,这个事件就不能在外部通过类的对象进行调用了,只能在类内部进行调用
  但是依然可以在外部进行注册

这里的CatShoutingEvent事件可以看做是在发布一个消息
Mouse()中的cat.CatShoutingEvent += this.RunAway; 可以看做是订阅了这个消息
  -- 事件的发布/订阅机制

事件与委托的联系和区别:

1. 事件是一种特殊的受限制的委托,是委托的一种特殊应用
  (不能在外部被调用的委托)

2. 常使用委托来进行回调
  比如做一个动画,动画完成后进行操作:
    因为动画需要时间来完成,因此传递给它一个委托,动画执行完后调用委托
    这个委托指向的方法就叫回调函数

  常使用事件来进行外发接口,给其他类进行注册

任务3:LINQ -- 数据查询
任务3-1&3-2:LINQ的基础使用(表达式) && 扩展写法(方法)

实例:武林高手数据查询

创建武林高手类 MartialArtMaster

class MartialArtMaster {
    public int Id {get; set;}
    public string Name {get; set; }
    public int age {get; set; }
    public string Menpai {get; set; }
    public string Kongfu {get; set; }
    public int level {get; set; }
}

创建武学类 Kongfu

class Kongfu {
    public int Id {get; set; }
    public string Name {get; set; }
    public int Lethality {get; set; } // 杀伤力
}

联系:要想知道武林高手的某个武学的杀伤力,需要通过两个类才能得知

在Main中初始化武林高手和武学


创建完数据,尝试使用LINQ进行数据查询

1. 查询武林高手中所有级别 level>8的:

普通方法:通过foreach遍历查找,将查找到的符合的武林高手存入res数组即可

LINQ方法 -- 表达式写法:

var res = from m in masterList  // 设置查询集合:在masterList列表中,m指代每个对象
          where m.level > 8  // where 查询条件
          select m;  // 把符合要求的m的集合返回
foreach(var element in res) {
    ... 输出
}  

若select语句为 select m.Name; 则返回的为m的Name属性 -- string[]

LINQ方法 -- 方法写法:

var res = masterList.Where(FilterMethod);
// FilterMethod是一个委托,方法需要满足 bool MethodName(MartialArtMaster m);
// 会遍历masterList中的所有元素,并作为参数传入FilterMethod
// 如果返回值为false,则被过滤掉

static bool FilterMethod(MartialArtMaster m) {
    return m.level > 8;
}

一般情况会将委托写成Lambda表达式的形式:

var res = masterList.Where( master => master.Level > 8);

2. 假设限制条件有多个

LINQ表达式:
  var res =   from m in masterList
       where m.Level > 8 && m.Menpai == "丐帮"
       select m.Name;

LINQ方法 + Lambda表达式:
  var res = masterList.Where(m => m.Level > 8 && m.Menpai = "丐帮");

LINQ方法也一样。

任务3-3&3-4:LINQ集合联合查询

LINQ联合查询:
  联合两个列表进行查询
  --  两个列表进行联合查询,结果是n*m条记录
    第一个列表的任意一条记录,会跟第二个列表的所有记录进行组合生成新的m个记录

var res =  from m in masterList
      from k in kongfuList
      select new {master = m, kongfu = k};
  // new 新建临时对象,一共两个字段,master和kongfu
  得到的结果为66条记录

但是有55条记录是没用的,这两个列表是有关联的 -- Kongfu的值

var res = from m in masterList
     from k in KongfuList
     where m.Kongfu == k.Name
     selece new {master = m, kongfu = k} ;
这时,返回的就只有11条记录了

实例:要取得技能杀伤力>90的武林高手名字

var res = from m in masterList
     from k in kongfuList
     where m.Kongfu == k.Name && k.Power > 90
     select m.Name;

联合查询的扩展方法的写法:

masterList.SelectMany()

public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector);

参数:两个Func委托;
  第一个Func:参数为第一个列表的元素,返回值为第二个列表
    用途:将第二个列表加入第一个列表,做联合查询
    m => kongfuList -- m不做任何操作(但也要传入),返回值为kongfuList
    m指的是masterList中的元素
  第二个Func:参数为两个列表的元素,返回值为需要返回的元素
    用途:进行联合查询
    (m, k) => new {master = m, kongfu = k}
    k指的是kongfuList中的元素
var res = masterList.SelectMany( m=>kongfuList, 
  (m,k)=>new {master=m, kongfu=k}); // res存放的为66条联合查询的记录

由于上面LINQ扩展方法返回的是结果列表
  可以直接进行Where()操作
  .Where(x => x.master.Kongfu == x.kongfu.Name);
    x指的是前面的SelectMany返回的66条记录中的元素
    // 此时,res存放的为11条过滤后的联合查询的记录
  再添加条件判断Power是否大于90
  .Where(x=>x.master.Kongfu == x.kongfu.Name && x.kongfu.Power > 90);

任务3-5:对结果进行排序 -- orderby

order by一般位于where关键词后

默认从小到大排序

实例:
  var res = from m in masterList
       where m.Level>8 && m.Menpai == "丐帮"
       orderby m.Age (descending)
       select m;
  // 如果在orderby后有descending关键字,则降序排序,否则默认为升序

按多个字段进行排序
  在orderby后用逗号把字段进行分割
  如:orderby m.Age, m.Level
  -- 按照age排序,如果age相同的,再按照level排序

扩展方法:OrderBy() / OrderByDescending()

var res = masterList.Where(m=>m.Level>8 && m.Menpai == "丐帮").OrderBy(m=>m.Age);

var res = masterList.Where(m=>m.Level>8 && m.Menpai == "丐帮").OrderBy(m=>m.Age).OrderBy(m=>m.Level);
可以用两个OrderBy方法进行多个字段排序吗?
  -- 不行,第二次OrderBy会把所有记录重新排序,覆盖了第一次的排序

解决方法:ThenBy()

var res = masterList.Where(m=>m.Level>8 && m.Menpai == "丐帮").OrderBy(m=>m.Age).ThenBy(m=>m.Level);

任务3-6:Join on集合联合查询

另一种联合查询:Join on

var res = from m in masterList
     join k in kongfuList on m.Kongfu equals k.Name
       select new {mastar=m, kongfu=k} ; 
  // 将masterList和kongfuList做连接,连接条件为 m.Kongfu equals k.Name

任务3-7&3-8:对结果进行分组操作 into groups && group by

分组查询1:into groups
实例:
  把武林高手按照所学功夫分类,看看那种功夫学习人数最多

1. 联合查询 -- 把masterList Join on KongfuList中
  var res = from k in kongfuList
       join m in masterList on k.Name equals m.kongfu
       select new {kongfu = k, master = m};
  // 得到每个功夫都有对应学习的武林高手

2. 对每种功夫进行分组 -- into groups
  var res = from k in kongfuList
       join m in masterList on k.Name equals m.kongfu
       into groups
       select new {kongfu = k, count = groups.Count() };
  // into groups 表示按照k.Name或m.kongfu进行分组
  // 因为已经分组了,因此无法继续输出m了
  // 定义字段count,保存groups.Count()值

3. 如果想要在按照所学人数的多少进行排序
  var res = from k in kongfuList
       join m in masterList on k.Name equals m.kongfu
       into groups
       orderby groups.Count()

       select new {kongfu = k, count = groups.Count() };

分组查询2:group by into

按照自身字段分组 -- 只对一个集合操作时,按照自身的字段的值进行分组

实例:将武林高手的集合按照武林门派进行分组,并返回每一门派的人数

var res = from m in masterList
     group m by m.Menpai into g
     select new {menpai = g.Key , count = g.Count())
  // 将m按照m.Menpai进行分组,放到g中
  // 因为已经进行了分组, 只有组的信息了,所以单个成员的属性是获取不到的
  // g.Key -- 表示按照哪个属性分的组

任务3-9:量词操作符 any all

用途:判断

Any() -- 
  实例:判断集合当中是否有属于丐帮的人

Any() 传入一个判断是非的委托

bool res = masterList.Any( m => m.Menpai == "丐帮");
  // 如果有,则返回true

All() --
  实例:判断集合当中是否都属于丐帮

All()使用方法和Any()相同

bool res = masterList.All( m => m.Menpai == "丐帮");

任务3-10:LINQ总结

一般微软的语言都支持LINQ的语法

LINQ可以支持从很多数据源进行查询
  上述例子我们都是对Objects进行查询
  还有xml, sql, dataset, entities等数据源
  LINQ to Objects部分的命名空间是System.Linq

任务4:反射和特性
任务4-1:反射和特性 -- Type类

任务4-2:反射和特性 -- Assembly程序集类

任务4-3:Obsolete特性

任务4-4:Contional特性

任务4-5:调用者信息特性

任务4-6:DebuggerStepThrough特性

任务4-7:创建自定义特性

任务5:线程、任务和同步
任务5-1:进程和线程的概念

进程 - Process

任何时刻,单核CPU只能运行一个进程,其他进程处于等待状态
  一个进程至少包含一个线程,也可以有多个线程
  一般情况下一个应用程序启用一个进程

同一个进程的内存空间对于它的进程来说是共享的,每个线程都可以享用这部分内存空间
  比如进程中的变量,是它的线程都可以访问的

互斥链 (Mutual Exclusion -- Mutex):防止多个线程同时读写某一块内存区域
  先使用的时候加锁,后使用的人看到锁就排队,直到锁打开再进行使用

信号量 (Semaphore):保证多个线程不会互相冲突
  某些内存区域只能供给固定数目的线程使用,超过的线程需要排队
  这时当线程在使用时,钥匙减1,没有钥匙时就需要排队了。

Mutex可以看成是Semaphore的一个特殊情况 (n=1),但是因为Mutex简单、效率高,因此能用mutex就用mutex

使用线程的情况:
示例:在Main函数中,一段代码用于下载文件,一段代码用于移动文件
  一个线程中的代码是从上到下执行的,于是需要等待文件下载完,才能执行其他代码
  -- 多线程:在一个线程中执行下载文件的代码,在另一个线程中移动文件,还有Main线程执行其他代码

-- 一般会对比较耗时的操作另外开启一个线程

任务5-2:线程开启方式1 -- 异步委托

通过委托开启线程 action.BeginInvoke();
  Action<int> a = Test;
  a.BeginInvoke(100, null, null); // 开启一个新的线程去执行a引用的方法
  // BeginInvoke的最后两个参数的作用见后,其他参数作为参数传递给引用方法

如果委托引用的方法有返回值 func.BeginInvoke();
  Func<int, int> f = Test;
  f.BeginInvoke(100, null, null);
  // 因为这个方法是异步执行的,因此新的线程可能需要很长的运行时间
  // 当新的线程执行完的时候,才能得到返回值
  // 返回值是IAsyncResult类型的,这个返回值可以取得线程的状态
  IAsyncResult ar = f.BeginInvoke(100, null, null);
  // 用循环进行判断
  while(!ar.IsCompleted) { // 新线程操作没有完成
    Thread.Sleep(10); // Main线程休息10ms,用来控制检测频率,不需要一直检测
  }
  // 新线程操作完成了,取得异步委托的返回值
  int res = f.EndInvoke(ar);

任务5-3:检测委托线程的结束 -- 通过等待句柄和回调函数

上一节中使用循环来监测线程是否结束
还有两种方式可以检测委托线程的结束:等待句柄和回调函数

等待句柄:

当我们通过BeginInvoke开启一个异步委托的时候,返回的结果是IAsyncResult,我们可以通过它的AsyncWaitHandle属性访问等待句柄。这个属性返回一个WaitHandler类型的对象,它的WaitOne()方法可以等待委托线程完成其任务,WaitOne方法可以设置一个超时时间作为参数(要等待的最长时间),如果发生超时就返回false。

bool isEnd = ar.AsyncWaitHandle.WaitOne(1000);
  -- 等待当前线程结束,再执行下面的代码
  -- 参数表示超时时间(ms),若等待超过这个时间,会直接返回true/false来表示线程是否结束

Func<int, int> f = Test;
IAsyncResult ar = f.BeginInvoke(100, null, null);
bool isEnd = ar.AsyncWaitHandle.WaitOne(1000);
if(isEnd) {
    int res = f.EndInvoke(ar);
}

异步回调方法:

BeginInvoke() 的最后两个参数,之前都是写的null
  倒数第二个参数是一个满足AsyncCallback委托的方法
  AsyncCallback委托定义了一个以IAsyncResult类型为参数,且返回类型是void,表示回调函数
  当线程结束的时候会调用这个委托指向的方法
  

Func<int, int> f = Test;
IAsyncResult ar = f.BeginInvoke(100, OnCallBack, null);

static void OnCallBack(IAsyncResult ar) {
}

怎么取得返回值呢? -- 在回调函数里面取得

倒数第一个参数用来给回调函数传递数据
对于最后一个参数,可以传递任意对象,以便从回调方法中访问它。
(我们可以设置为委托实例,这样就可以在回调方法中获取委托方法的结果)

Func<int, int> f = Test;
IAsyncResult ar = f.BeginInvoke(100, OnCallBack, f);

static void OnCallBack(IAsyncResult ar) {
    Func<int, int> f = as.AsyncState as Func<int, int>;
    int res = f.EndInvoke(ar);
    Console.WriteLine(res);
}

将回调函数改写成Lambda表达式:-- 更加简便

Func<int, int> f = Test;
f.BeginInvoke(100, ar => {
    int res = f.EndInvoke(ar);
    Console.WriteLine(res);
    } , null);
// 因为Lambda表达式可以直接访问到外部变量
// 因此没有必要通过最后一个参数传递f进去

任务5-4:线程开启方法2 -- 通过Thread类

通过Thread开启线程 -- System.Threading;

// 创建线程,但并没有启动
Thread thread = new Thread(DownloadFile); // 传入委托给构造函数
// 启动线程
thread.Start();

获取线程ID:
  在线程中,Thread.CurrentThread.ManagedThreadId.

传递参数1:-- 通过Start()传递,参数的类型必须为object类型
  Thread thread = new Thread(DownloadFile); // 不变
  thread.Start("....");  // 通过Start传递参数
  static void DownloadFile(object filename) {...}

传递参数2:-- 通过新建类,并将线程的方法定义为类中的实例方法
  新建类,将所有需要传递的参数以成员的方式存在新建类中

class MyThread {
    private string filename;
    private string filepath;

    public MyThread(string name, string path) {
        filename = name;
        filepath = path;
    }

    public void DownloadFile() {
        方法体 -- 可以直接访问私有数据
    }
}

此时在外部将对象的普通方法作为委托传递给Thread构造函数即可(之前的是静态方法)
  MyThread myThread = new MyThread("name", "path");
  Thread thread = new Thread(myThread.DownloadFile);
  thread.Start();

通过Lambda表达式:
  Thread thread = new Thread(()=>{
    方法体;
    });
  thread.Start();

任务5-5:线程的其他概念 -- 后台前台线程、线程的优先级、线程的状态

前台线程和后台线程:
  有一个前台线程在运行的话,应用程序的进程就在运行
    即如果有前台线程在运行,但是Main方法结束了,那么应用程序的进程仍然在运行,直到所有线程结束
  但是如果Main方法结束了,那么后台线程会被强制结束
    当所有的前台线程运行完毕,如果还有后台线程运行的话,所有的后台线程会被终止掉。

默认情况下:
  使用Thread类创建的线程是前台线程
  使用线程池创建的线程是后台线程

用Thread创建线程的时候,可以通过.IsBackground来设置前台/后台
  Thread thread = new Thread(...);
  thread.IsBackground = true; // 设置为后台线程

线程的优先级:
  线程由操作系统调度,一个CPU同一时间只能运行一个线程。
  当有很多线程需要CPU去执行的时候,线程调度器会根据线程的优先级去判断先去执行哪一个线程
  如果优先级相同的话,就使用一个循环调度规则,逐个执行每个线程。(每个线程分配时间相同)

在Thead类中,可以设置Priority属性 (线程的基本优先级)。
  Priority属性是一个ThreadPriority枚举定义的一个值。
  定义的级别有Highest, AboveNormal, BelowNormal 和 Lowest。

控制线程:

1. 获取线程的状态 -- Running / Unstarted
  当调用thread.Start()后,新线程处于Unstarted状态
  当操作系统的线程调度器选择了要运行的线程,才会变为Running状态
  使用Thread.Sleep() 可以让当前线程休眠进入WaitSleepJoin状态

2. 使用 thread.Abort() 强制停止线程
  调用这个方法,会在终止要终止的线程中抛出一个ThreadAbortException类型的异常
  我们可以try catch这个异常,然后在线程结束前做一些清理的工作。

3. 如果需要等待线程的结束,可以调用 thread.Join()
  表示把Thread加入进来,停止当前线程,并把它设置为WaitSleepJoin状态
  直到加入的线程完成为止,再继续执行下面的代码

任务5-6:线程开启方法3 -- 线程池

创建线程需要时间。
如果有不同的小任务要完成,就可以事先创建许多线程 , 在应完成这些任务时发出请求。
  这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。

系统自带一个ThreadPool类,用于管理线程。
  这个类会在需要时增减池中线程的线程数,直到达到最大的线程数。
  在双核 CPU中,默认设置为1023个工作线程和 1000个 I/o线程。
  也可以手动指定在创建线程池时应该立即启动的最小线程数,以及线程池中可用的最大线程数。
  如果有更多的作业要处理,且线程池中线程的个数也到了极限,
    那么最新的作业就要排队,且必须等待线程完成其任务。

线程池默认创建出来的任务都是后台线程
注意:不能把线程池的线程修改为前台线程
  不能修改线程池中线程的优先级和名称
  线程池中的线程只能用于运行时间段的任务

实例:开启工作线程

static void ThreadMethod(object state) {
  // 需要一个object类的参数
}

Main() {
  ThreadPool.QueueUserWorkItem(ThreadMethod);
  // 如果想要传递数据,在ThreadMethod后写上逗号和实参即可

任务5-7&5-8:线程开启方法4 -- 任务

通过任务开启线程 -- 三种方式

第一种 -- 和Thread有些类似
  任务Task其实是线程的一种封装
  Task t = new Task(TaskMethod); // 将委托指向的方法传递过来
  t.Start();

第二种 -- 通过TaskFactory 工厂模式
  TaskFactory tf = new TaskFactory();
  // 通过任务工厂 TaskFactory 可以对许多task进行操作
  Task t = tf.StartNew(TaskMethod);

连续任务:
如果一个任务t1的执行时依赖于另一个任务t2的,即t1需要在t2执行完成后才开始执行
   -- 使用连续任务解决 -- task.ContinueWith()
实例:
  Task t1 = new Task(DoFirst);
  Task t2 = t1.ContinueWith(DoSecond);
  Task t3 = t1.ContinueWith(DoSecond);
  Task t4 = t1.ContinueWtih(DoError, TaskContinuationOptions.OnlyOnFaulted);

任务层次结构:
在一个任务中启动另一个新的任务,即新的任务为当前任务的子任务
若父任务执行完,但子任务没有执行完,则父任务状态会设置为WaitingForChildrenToComplete;当子任务也执行完了,则父任务的状态为RunToCompletion

实例:

static void Main() {
    var parent = new Task(ParentTask);
    partent.Start();
    Thread.Sleep(2000);
    Console.WriteLine(parent.Status);
    Thread.Sleep(4000);
    Console.WriteLine(parent.Status);
}
static void ParentTask() {
    var child = new task(ChildTask);
    child.Start();
    Thread.Sleep(1000);
}
static void ChildTask() {
    Thread.Sleep(5000);
}

任务5-9:线程中会遇到的问题 -- 争用条件和死锁

争用条件 Race Conditions:
  A race condition occurs when two threads access a shared variable at the same time.
  多个线程同时访问一个变量时,造成的读写冲突

实例:

线程要执行的委托指向的方法:

class MyThreadObject {
    private int state = 5; // shared variable
    
    public void ChangeState() {
        state++;
        if(state == 5) { // 目前来看这个条件永远不会满足
            输出state=5;
        }
        state = 5;
    }
}

在Main中通过线程执行上述方法:

class Program {
    ... Main ... {
        MyThreadObject myThread = new MyThreadObject();
        Task t = new Task(ChangeState);
        t.Start(myThread); // 启动线程

    // 定义函数,执行多次myThread.ChangeState()
    static void ChangeState(Object o) {
        // 把Object类型转换成原来类型
        MyThreadObject myThread = o as MyThreadObject;
        while(true) {
            myThread.ChangeState);
        }
    }
}

运行:没有任何输出 -- 因为只有一个线程时按照顺序执行,判断处并不会出现state=5的情况

修改 -- 增加一个线程,同时执行ChangeState()
  增加一个 Task t = new Task(ChangeState); t.Start(myThread);
    或使用TaskFactory

运行:输出大量的state=5
原因:当一个线程执行到state=5的时候,另一个线程正好进行判断condition

争用条件的解决方法 -- 对当前的shared对象加锁

锁 lock():向系统申请对指定对象加锁,锁定该对象
  如果对象没有被锁定,那么可以j进行访问
  如果对象被锁定了,则需要等待锁定解除后才能进行访问

实例:lock(object) {}

while(true) {
    lock(myThread) { // 对myThread对象加锁
        myThread.ChangeState(); // 同一时间,只能有一个线程执行这个操作
    } // 解锁
}

死锁 Deadlocks:由锁产生的问题
  当两个线程同时申请到了一个锁,而两个线程在解锁之前遇到了另一个锁又是对方申请到的第一
    个锁,此时两个线程都在等待对方解锁第一个锁,产生了死锁

实例:

public class SampleThread{
    private StateObject s1;
    private StateObject s2;
    public SampleThread(StateObject s1,StateObject s2){
        this.s1= s1;
        this.s2 = s2;
    }
    public void Deadlock1(){
    int i =0;
    while(true){
            lock(s1){ // 先锁s1
                lock(s2){ // 后锁s2
                    s1.ChangeState(i);
                    s2.ChangeState(i);
                    i++;
                    Console.WriteLine("Running i : "+i);
    }}}}}
    public void Deadlock2(){
        int i =0;
        while(true){
            lock(s2){ // 先锁s2
                lock(s1){ // 后锁s1
                    s1.ChangeState(i);
                    s2.ChangeState(i);
                    i++;                                                                                    
                    Console.WriteLine("Running i : "+i);
    }}}}}

Main... {
    var state1 = new StateObject();
    var state2 = new StateObject();
    new Task(new SampleTask(s1,s2).DeadLock1).Start();
    new Task(new SampleTask(s1,s2).DeadLock2).Start();    
}

解决方法:
  在编程的开始设计阶段,设计锁定的顺序即可
  比如上例中:
    两个方法都必须按照先锁定s1后锁定s2的顺序(或反过来)

任务6:网络

任务7:文件操作

任务8:xml操作、jason操作和excel操作

原文地址:https://www.cnblogs.com/FudgeBear/p/8884420.html