C#基本语法整理-高级用法的梳理

702 阅读15分钟

一.引言

C#还剩余一些高级用法,比如事件和Lambda表达式,以及反射,泛型约束,这些语法C#比较特别

二.泛型约束

泛型的基本知识点比较简单,和其他面向对象语言并无区别,主要是罗列如下不同点

  • 可以使用typeof(T)来进行比对是否是某种你想要的类型,可做switch分流处理
  • 几种泛型约束
    //让泛型的类型有一定的限制
    //关键字:where
    //泛型约束一共有6种
    //1.值类型                              where 泛型字母:struct
    //2.引用类型                            where 泛型字母:class
    //3.存在无参公共构造函数                 where 泛型字母:new()
    //4.某个类本身或者其派生类               where 泛型字母:类名
    //5.某个接口的派生类型                  where 泛型字母:接口名
    //6.另一个泛型类型本身或者派生类型       where 泛型字母:另一个泛型字母
    // 基本格式
    // where 泛型字母:(约束的类型)
    
    唯一想说的就是第6种泛型约束以及存在多个泛型约束的写法形式
    //存在多个泛型约束的写法
    class Test8<T, K> where T : class, new() where K : struct
    {
    
    }
    // 另一个泛型类型本身或者派生类型
    class Test6<T, U> where T : U
    {
        public T value;
    
        public void TestFun<K, V>(K k) where K : V
        {
    
        }
    }
    // 那么使用的时候,K类型必须是V类型本身或者它的子类
    
  • 通过泛型约束实现单例模式的基类
    abstract class SingleBase<T> where T : class
    {
        private static readonly object lockObj = new object();
    
        private static T instance;
    
        protected SingleBase()
        {
    
        }
    
        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    lock(lockObj)
                    {
                        if (instance == null)
                        {
                            instance = Activator.CreateInstance(typeof(T), true) as T;
                        }
                    }
                }
                return instance;
            }
    
            private set { }
        }
    }
    
    上述方式创建Instance实例时使用了反射,目前来看,只有这种写法才能保证子类可以私有化构函数
    泛型约束代码链接

三.委托

C#中的委托可以说是一种变量类型,类似于Kotlin中的函数类型,那既然是函数类型,顾名思义是用来承载一个函数的,通常承载匿名函数 // 目前来说,记住它的功能即可 // 1.声明一个委托类型(等价于Kotlin中的函数类型) public delegate void TestFunc(); // 2.定义一个委托变量,并让它等于一个函数名 TestFunc testFucn = Show; void Show() { Console.WriteLine("hello world"); } // 委托还有多播的功能,简言之就是可以+多个函数,调用委托变量,会一次性执行我们+的所有函数,详情看代码链接即可 后续会使用Lambda表达式即匿名函数的方式来替换函数名给委托变量赋值,另外,系统给我们提供了一系列委托类型,很多时候我们不需要自定义委托类型,直接使用系统提供的即可,这里就不列出了,详情看代码链接
委托代码链接

四.事件-event

事件是基于委托使用的,使用是一样的,它有如下特点

  • 委托的安全包裹
  • 让委托的使用更具有安全性
  • 是一种特殊的变量类型 语法如下:
    //访问修饰符 event 委托类型 事件名;
    //事件的使用:
    //1.事件是作为 成员变量存在于类中
    //2.委托怎么用 事件就怎么用
    //事件相对于委托的区别:
    //1.不能在类外部 赋值
    //2.不能再类外部 调用(需要暴露给外部一个API)
    //3.只能在外部通过 + -对其操作
    //4.事件不能用作声明临时变量
    //注意:
    //它只能作为成员存在于类和接口以及结构体中

所以委托的出现是为了防止外部随意置空委托,防止外部随意调用委托,事件相当于对委托进行了一次封装,让其更加安全

五.匿名函数和Lambda表达式

C#中的匿名函数和Lambda表达式和Kotlin中有异曲同工之妙,还是逐步从匿名函数到Lambda表达式过渡
匿名函数顾名思义就是没有名字的函数,主要是配合委托和事件进行使用,脱离委托和事件是不会使用匿名函数的

  • 基本语法
            //delegate (参数列表)
            //{
            //    //函数逻辑
            //};
    
  • 基本使用
            //1.无参无返回
            //这样申明匿名函数 只是在申明函数而已 还没有调用
            //真正调用它的时候 是这个委托容器啥时候调用 就什么时候调用这个匿名函数
            Action a = delegate ()
            {
                Console.WriteLine("匿名函数逻辑");
            };

            a();
            //2.有参
            Action<int, string> b = delegate (int a, string b)
            {
                Console.WriteLine(a);
                Console.WriteLine(b);
            };

            b(100, "123");
            //3.有返回值
            Func<string> c = delegate ()
            {
                return "123123";
            };

            Console.WriteLine(c());

            //4.一般情况会作为函数参数传递 或者 作为函数返回值
            Test t = new Test();
            Action ac = delegate ()
            {
                Console.WriteLine("随参数传入的匿名函数逻辑");
            };
            t.Dosomthing(50, ac);
            //  参数传递
            t.Dosomthing(100, delegate ()
            {
                Console.WriteLine("随参数传入的匿名函数逻辑");
            });

            //  返回值
            Action ac2 = t.GetFun();
            ac2();
            //一步到位 直接调用返回的 委托函数
            t.GetFun()();

目前使用匿名函数的缺点就是委托多播时无法-去已经添加过的匿名函数,无法移除
注意如下的写法会改变临时变量的生命周期,即使外层函数执行完毕也不会释放变量的内存

        static Func<int, int> TestFun(int i)
        {
            //这种写法会改变 i的生命周期
            return delegate (int v)
            {
                return i * v;
            };
        }
  • Lambda表达式
    Lambda表达式是匿名函数的精简写法
             //lambad表达式
            //(参数列表) =>
            //{
            //    //函数体
            //};
            //1.无参无返回
            Action a = () =>
            {
                Console.WriteLine("无参无返回值的lambad表达式");
            };
            a();
            //2.有参
            Action<int> a2 = (int value) =>
            {
                Console.WriteLine("有参数Lambad表达式{0}", value);
            };
            a2(100);

            //3.甚至参数类型都可以省略 参数类型和委托或事件容器一致
            Action<int> a3 = (value) =>
            {
                Console.WriteLine("省略参数类型的写法{0}", value);
            };
            a3(200);
            //4.有返回值
            Func<string, int> a4 = (value) =>
            {
                Console.WriteLine("有返回值有参数的那么大表达式{0}", value);
                return 1;
            };
  • 闭包
    内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止
    注:该变量提供的值并非变量创建时的值,而是在父函数范围内的最终值。
    简单说就是内部创建的Lambda表达式会引用外层的变量,可以从内存GC的角度来理解,存在引用,自然就不会释放,下面通过一个小例子来演示
    class Test
    {
        public event Action action;

        public Test()
        {
            int value = 10;
            //这里就形成了闭包
            //因为 当构造函数执行完毕时  其中申明的临时变量value的声明周期被改变了
            action = () =>
            {
                Console.WriteLine(value);
            };

            for (int i = 0; i < 10; i++)
            {
                //此index 非彼index
                // int index = i;
                action += () =>
                {
                    Console.WriteLine(i);
                };
            }
        }

        public void DoSomthing()
        {
            action();
        }
    }

上述代码,如果没有打开index那行注释,最终输出的i都是10,其实就是每个Lambda表达式使用的是同一个变量i,所以为了让各自输出的结果不同,就各自定义一个属于自己的临时变量index

  • 解决委托多播时获取每个Lambda表达式的返回值
    这个问题的关键点在于如何获取到每一个委托,系统给我们提供了一个遍历每一个委托的方法
    GetInvocationList()可以返回一个委托数组,具体怎么使用,这里就不展示了,使用foreach增强for循环单独调用每一个委托元素即可

六.注解&反射&迭代器

  • 泛型的协变和逆变
    Java和Kotlin中已经接触过了,这里就简单进行一个总结,主要是两个维度
    1. 从函数(委托同理)参数和返回值的角度
      使用out修饰泛型,只能该泛型类型的变量只能用作返回值,in修饰只能用作参数传入,在Kotlin中有一个比较好的说法就是out是生产者
    2. 从里氏替换原则的角度来看
      out指泛型类的容器,比如List,容器中的元素存在父子关系,那么使用out修饰,泛型子类的容器可以赋值给泛型父类的容器 协变可以从谐音的角度来理解,“谐变”,很和谐的变化,从里氏替换原则的角度来说,子类向父类转就是一个和谐的过程,反之则是逆变
  • 预处理指令
    书面一点的解释就是可以让代码还没有编译之前就可以进行一些预处理判断,目前来看还是挺有用的,比如跨平台的游戏,针对不同的平台或者版本号执行的逻辑,这种情况按照常理来说应该在程序运行时来进行实际的真正判断,但是,这样有一个不好的地方,是不是会将所有版本或者平台的代码进行编译,很显然,这不符合我们的预期,那么预处理就是有这个好处,在编译之前就进行判断,只会编译符合条件的部分代码,下面通过示例来展示
        // 1.#define用于定义一个无值得变量,可以理解为仅仅是一个符号
        #define IOS
        #define Android
        #define PC
        // #undef是取消定义一个符号,简单说就是移除前面定义的某个符号
        // 2. 有了符号定义,我们还需要搭配条件判断来做具体的逻辑处理,也就是我们需要编译的那一部分
        #if Android
                    Console.WriteLine("平台为Android");
        #elif IOS || PC
                    Console.WriteLine("平台为IOS 或 PC端");
        #else
                    Console.WriteLine("其它野鸡平台");
                     //#warning 这个平台 不合法
                    //#error 这个平台不准执行
        #endif
    
    基本上常用的预处理指令如上述,仅仅定义一个符号没有意义,得搭配判断进行处理,多个判断符号之间可以用逻辑或、逻辑与连接,我们常用的#region仅仅是提供一个注释说明的效果,不影响代码编译,此外要注意的是判断符之间是成对搭配使用的,有开始就有结尾,有始有终
  • 反射相关
    C#中的反射处理也是挺有特色,基本上大差不差,都是通过Type来进行操作,无论是调用构造函数,还是成员函数,亦或是静态方法,形式差不多,记住一些常用的API即可,下面进行一个简单的总结
    1. 先解释一些基本概念
      程序集就是我们写的一个代码集合,我们现在写的所有代码,最终看到的形态就是一个dll(动态链接库),类似于Android中的一个依赖库lib,我们通过反射就可以操作dll中的数据(类,方法等)
      元数据就是用来描述数据的数据,就比如说Java中有一个东西是对类的抽象,称之为Class
      关于反射就是在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
    2. Type:类的信息类,反射中的操作基本上都是通过它进行的,它保存了一个类所有的信息,包括成员变量、成员函数、构造函数以及静态方法,怎么去做,其实就是调用Type中提供的一系列API
          #region 获取Type
          //1.万物之父object中的 GetType()可以获取对象的Type
          int a = 42;
          Type type = a.GetType();
          Console.WriteLine(type);
          //2.通过typeof关键字 传入类名 也可以得到对象的Type
          Type type2 = typeof(int);
          Console.WriteLine(type2);
          //3.通过类的名字 也可以获取类型
          //  注意 类名必须包含命名空间 不然找不到
          Type type3 = Type.GetType("System.Int32");
          Console.WriteLine(type3);
      
          #endregion
      
          #region 得到类的程序集信息
          //可以通过Type可以得到类型所在程序集信息
          Console.WriteLine(type.Assembly);
          Console.WriteLine(type2.Assembly);
          Console.WriteLine(type3.Assembly);
          #endregion
      
          #region 获取类中的所有公共成员
          //首先得到Type
          Type t = typeof(Test);
          //然后得到所有公共成员
          //需要引用命名空间 using System.Reflection;
          MemberInfo[] infos = t.GetMembers();
          for (int i = 0; i < infos.Length; i++)
          {
              Console.WriteLine(infos[i]);
          }
          #endregion
      
          #region 获取类的公共构造函数并调用
          //1.获取所有构造函数
          ConstructorInfo[] ctors = t.GetConstructors();
          for (int i = 0; i < ctors.Length; i++)
          {
              Console.WriteLine(ctors[i]);
          }
      
          //2.获取其中一个构造函数 并执行
          //得构造函数传入 Type数组 数组中内容按顺序是参数类型
          //执行构造函数传入  object数组 表示按顺序传入的参数
          //  2-1得到无参构造
          ConstructorInfo info = t.GetConstructor(new Type[0]);
          //执行无参构造 无参构造 没有参数 传null
          Test obj = info.Invoke(null) as Test;
          Console.WriteLine(obj.j);
      
          //  2-2得到有参构造
          ConstructorInfo info2 = t.GetConstructor(new Type[] { typeof(int) });
          obj = info2.Invoke(new object[] { 2 }) as Test;
          Console.WriteLine(obj.str);
      
          ConstructorInfo info3 = t.GetConstructor(new Type[] { typeof(int), typeof(string) });
          obj = info3.Invoke(new object[] { 4, "444444" }) as Test;
          Console.WriteLine(obj.str);
          #endregion
      
          #region 获取类的公共成员变量
          //1.得到所有成员变量
          FieldInfo[] fieldInfos = t.GetFields();
          for (int i = 0; i < fieldInfos.Length; i++)
          {
              Console.WriteLine(fieldInfos[i]);
          }
          //2.得到指定名称的公共成员变量
          FieldInfo infoJ = t.GetField("j");
          Console.WriteLine(infoJ);
      
          //3.通过反射获取和设置对象的值
          Test test = new Test();
          test.j = 99;
          test.str = "2222";
          //  3-1通过反射 获取对象的某个变量的值
          Console.WriteLine(infoJ.GetValue(test));
          //  3-2通过反射 设置指定对象的某个变量的值
          infoJ.SetValue(test, 100);
          Console.WriteLine(infoJ.GetValue(test));
          #endregion
      
          #region 获取类的公共成员方法
          //通过Type类中的 GetMethod方法 得到类中的方法
          //MethodInfo 是方法的反射信息
          Type strType = typeof(string);
          MethodInfo[] methods = strType.GetMethods();
          for (int i = 0; i < methods.Length; i++)
          {
              Console.WriteLine(methods[i]);
          }
          //1.如果存在方法重载 用Type数组表示参数类型
          MethodInfo subStr = strType.GetMethod("Substring",
              new Type[] { typeof(int), typeof(int) });
          //2.调用该方法
          //注意:如果是静态方法 Invoke中的第一个参数传null即可
          string str = "Hello,World!";
          //第一个参数 相当于 是哪个对象要执行这个成员方法
          object result = subStr.Invoke(str, new object[] { 7, 5 });
          Console.WriteLine(result);
      
          #endregion
      
      这里就直接通过代码整理的方式来呈现Type的一些列使用
    3. Activator
      该类主要是用来快速实例化一个对象,只需要Type以及知道对应的构造函数参数即可
         //先得到Type
         //然后 快速实例化一个对象
         Type testType = typeof(Test);
         //1.无参构造
         Test testObj = Activator.CreateInstance(testType) as Test;
         Console.WriteLine(testObj.str);
         //2.有参数构造
         testObj = Activator.CreateInstance(testType, 99) as Test;
         Console.WriteLine(testObj.j);
      
         testObj = Activator.CreateInstance(testType, 55, "111222") as Test;
      
    4. Assembly
      程序集类,用于加载一个dll,在Untiy中会常见dll,下面展示一下如何使用
      // 要加载的程序集代码
      namespace CSharpDLL
      {
          public class Class1
          {
              int age = 10;
              public void Speak()
              {
                  Console.WriteLine("hello CSharp World");
              }
          }
      }
      Assembly assembly = Assembly.LoadFrom(@"具体路径\CSharpDLL");
          Type[] types = assembly.GetTypes();
          for (int i = 0; i < types.Length; i++)
          {
              Console.WriteLine(types[i]);
          }
          Type type = assembly.GetType("CSharpDLL.Class1");
          MethodInfo[] methods = type.GetMethods();
          for (int i = 0; i < methods.Length; i++)
          {
              Console.WriteLine(methods[i]);
          }
          object testClass = Activator.CreateInstance(type);
          MethodInfo method = type.GetMethod("Speak");
          method.Invoke(testClass, null);
      
  • 特性(注解)
    C#中的特性就是平常使用的注解,从结构上来说都是类似的,唯一不同的就是语法以及API,下面进行一个简单整理
    1. 自定义特性
    //继承特性基类 Attribute
    // 元注解
    //参数一:AttributeTargets —— 特性能够用在哪些地方
    //参数二:AllowMultiple —— 是否允许多个特性实例用在同一个目标上
    //参数三:Inherited —— 特性是否能被派生类和重写成员继承
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
    class MyCustomAttribute : Attribute
    {
        //特性中的成员 一般根据需求来写
        public string info;
    
        public MyCustomAttribute(string info)
        {
            this.info = info;
        }
    
        public void TestFun()
        {
            Console.WriteLine("特性的方法");
        }
    }
    
    注意:通常都会给特性名后面加上Attribute这个单词,但是实际使用的时候系统会为我们自动截去,上面同时展示了元特性,也就是元注解,作用于注解的注解,一般用来指定当前特性可以用于何处
    2. 关于注解的标识处就不介绍了,下面整理一下系统提供给我们的特性
    #region 系统自带特性——过时特性
    //过时特性
    //Obsolete
    //用于提示用户 使用的方法等成员已经过时 建议使用新方法
    //一般加在函数前的特性
    
    class TestClass
    {
        //参数一:调用过时方法时 提示的内容
        //参数二:true-使用该方法时会报错  false-使用该方法时直接警告
        [Obsolete("OldSpeak方法已经过时了,请使用Speak方法", false)]
        public void OldSpeak(string str)
        {
            Console.WriteLine(str);
        }
    
        public void Speak()
        {
    
        }
    
        public void SpeakCaller(string str, [CallerFilePath] string fileName = "",
            [CallerLineNumber] int line = 0, [CallerMemberName] string target = "")
        {
            Console.WriteLine(str);
            Console.WriteLine(fileName);
            Console.WriteLine(line);
            Console.WriteLine(target);
        }
    }
    
    #endregion
    
    #region 系统自带特性——调用者信息特性
    //哪个文件调用?
    //CallerFilePath特性
    //哪一行调用?
    //CallerLineNumber特性
    //哪个函数调用?
    //CallerMemberName特性
    
    //需要引用命名空间 using System.Runtime.CompilerServices;
    //一般作为函数参数的特性
    #endregion
    
    #region 系统自带特性——条件编译特性
    //条件编译特性
    //Conditional
    //它会和预处理指令 #define配合使用
    
    //需要引用命名空间using System.Diagnostics;
    //主要可以用在一些调试代码上
    //有时想执行有时不想执行的代码
    #endregion
    
    #region 系统自带特性——外部Dll包函数特性
    //DllImport
    
    //用来标记非.Net(C#)的函数,表明该函数在一个外部的DLL中定义。
    //一般用来调用 C或者C++的Dll包写好的方法
    //需要引用命名空间 using System.Runtime.InteropServices
    #endregion
    
    1. 注解搭配反射使用,这也是经常会使用到的
            #region 特性的使用
            MyClass mc = new MyClass();
            Type t = mc.GetType();
            //t = typeof(MyClass);
            //t = Type.GetType("Lesson21_特性.MyClass");
    
            //判断是否使用了某个特性
            //参数一:特性的类型
            //参数二:代表是否搜索继承链(属性和事件忽略此参数)
            if (t.IsDefined(typeof(MyCustomAttribute), false))
            {
                Console.WriteLine("该类型应用了MyCustom特性");
            }
    
            //获取Type元数据中的所有特性
            object[] array = t.GetCustomAttributes(true);
            for (int i = 0; i < array.Length; i++)
            {
                if (array[i] is MyCustomAttribute)
                {
                    Console.WriteLine((array[i] as MyCustomAttribute).info);
                    (array[i] as MyCustomAttribute).TestFun();
                }
            }
            #endregion
    
  • 迭代器
    C#中提供了专门的接口用于实现一个迭代器,基本原理和Java中的Iterator差不多,下面展示如何实现
     #region 标准迭代器的实现方法
    //关键接口:IEnumerator,IEnumerable
    //命名空间:using System.Collections;
    //可以通过同时继承IEnumerable和IEnumerator实现其中的方法
    
    class CustomList : IEnumerable, IEnumerator
    {
       private int[] list;
       //从-1开始的光标 用于表示 数据得到了哪个位置
       private int position = -1;
    
       public CustomList()
       {
           list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
       }
    
       #region IEnumerable
       public IEnumerator GetEnumerator()
       {
           Reset();
           return this;
       }
       #endregion
    
       public object Current
       {
           get
           {
               return list[position];
           }
       }
       public bool MoveNext()
       {
           //移动光标
           ++position;
           //是否溢出 溢出就不合法
           return position < list.Length;
       }
    
       //reset是重置光标位置 一般写在获取 IEnumerator对象这个函数中
       //用于第一次重置光标位置
       public void Reset()
       {
           position = -1;
       }
    }
    #endregion
    
    上面的写法是很标准的,还有一种语法糖的写法,可以省去很多代码
    #region 用yield return 语法糖实现迭代器
    //yield return 是C#提供给我们的语法糖
    //所谓语法糖,也称糖衣语法
    //主要作用就是将复杂逻辑简单化,可以增加程序的可读性
    //从而减少程序代码出错的机会
    
    //关键接口:IEnumerable
    //命名空间:using System.Collections;
    //让想要通过foreach遍历的自定义类实现接口中的方法GetEnumerator即可
    
    class CustomList2 : IEnumerable
    {
       private int[] list;
    
       public CustomList2()
       {
           list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
       }
    
       public IEnumerator GetEnumerator()
       {
           for (int i = 0; i < list.Length; i++)
           {
               //yield关键字 配合迭代器使用
               //可以理解为 暂时返回 保留当前的状态
               //C#的语法糖
               yield return list[i];
           }
       }
    }
    
    #endregion
    
    使用上的表现,上述两个类其实和一个List很像,所以可以直接使用foreach进行遍历,使用foreach本质就是使用迭代器进行元素遍历,此外,如果想实现一个泛型迭代器,直接将内部定义的数组设置为泛型数组即可。
  • 关于其余补充的特殊用法,详情看代码链接高级用法代码链接