zoukankan      html  css  js  c++  java
  • 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操作

  • 相关阅读:
    谈谈surging引擎的tcp、http、ws协议和如何容器化部署
    Surging如何使用Swagger 组件测试业务模块
    Ocelot简易教程(七)之配置文件数据库存储插件源码解析
    [转载]Ocelot简易教程(六)之重写配置文件存储方式并优化响应数据
    [转载]Ocelot简易教程(五)之集成IdentityServer认证以及授权
    [转载]Ocelot简易教程(四)之请求聚合以及服务发现
    [转载]Ocelot简易教程(三)之主要特性及路由详解
    [转载]Ocelot简易教程(二)之快速开始2
    [转载]Ocelot简易教程(二)之快速开始1
    Next Permutation
  • 原文地址:https://www.cnblogs.com/FudgeBear/p/8884420.html
Copyright © 2011-2022 走看看