C#学习笔记 - 杂

主要记录一些其他语言没有或用法不一样的新玩意

以及常用和重点内容

结合《C#图解教程(第4版)》学习

1. C#中的foreach循环

  • 迭代变量是临时的而且是只读的
  • 但对于引用类型的数据,引用是只读的,但是数据是可以修改的
int[] nums = new int []{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
foreach (int item in nums) {
	Console.WriteLine(item);
}

2. C#中的高维数组

  • C#中数组只能放在类型后,而不能放在变量名后
  • 数组的长度不能写在类型声明的方括号中,只能写在实例化的方括号中
// 1. 交错数组
int[][] matrix = new int [][] {		// 或者写[2][],但是不能些[2][3]
    new int [] {1, 2, 3},			// 或者写[3]
    new int [] {4, 5, 6}
};
// 2. 矩形数组
int [,] matrix2 = new int [2, 3] {	// 或者写[,],但是不能写[2,]或[,3]
    {1, 2, 3},
    {4, 5, 6}
};

3. 参数数组,即任意个数参数

void fun(params int[] nums) {
    // ... 
}

4. 结构体

  • 在C#结构体的构造函数

    • 构造函数必须对所有成员进行初始化

    • 和类不同的是,给结构体提供了其他构造函数时,系统提供的默认构造函数仍然保留,如果再给定一个无参的构造函数会报错

    • 但是如果通过默认参数的方式指定一种看上去无参的构造函数,仍然会调用系统的默认构造函数(基本类型都是默认值,引用类型都是null),如下面的例子,输出结果就是0

    • struct S {
          public int x;
          public S(int x = 20) {
              this.x = x;
          }
      }
      class Program {
          static void Main(string[] args) {
              S s = new S();
              System.Console.WriteLine(s.x);
          }  
      }
      
  • 和C++不同的是,C#中结构体的字段默认是private类型

  • C#中结构体字段不能直接初始化,要在构造函数中赋值

  • 结构体类型为值类型,而非引用类型

    • 可以不使用new,但是class必须使用new
    • 不能赋值为null,且不能引用同一对象,赋值为值拷贝
  • 结构体是密封的,不能被派生,也不能手动派生自其他类型(结构体隐式派生自ValueType),但是可以实现其他接口

  • 如果想将结构实例作为引用类型对象,需要创建装箱装箱和拆箱 放在后面解释

  • 结构的开销小于类,但装箱拆箱代价较高

  • 预定义简单类型被.NET实现为结构

  • 可以声明分布结构类似分部类

5. 字符串及处理方法

6. 运算符重载

  • public static Type operator + (Type a, Type b) {
      // ...
    }
    
  • 只能用于class或struct

  • 一元运算符要带一个class或struct类型的参数

  • 二元运算符两个参数,其中至少一个为class或struct类型

  • 同时使用public和static

  • 作为要操作的类的成员

  • 运算符重载限制

    • 不能创建新运算符,也不能修改运算符的语法规则用法
    • 不能重载预定义类型的运算符
    • 不能该改变运算符的优先级或者结合性

7. 索引器

  • public Type this[Type param1, ...] {
        get { }
        set { }
    }
    
  • 不能声明为static

  • 与属性一样同样有一个隐藏参数value

  • 注意自己实现索引器时,尤其是访问数组和列表时要记得处理异常

  • 可以根据参数列表不同进行重载

8. dynamic类型

  • 由于.NET框架中有动态类型语言如IronPython和IronRuby等,C#要使用这些语言的程序集时可以使用dynamic关键字

9. 预定义简单类型

  • sbyte、byte、short、ushort、int、uint、long、ulong:有符号/无符号 8、16、32、64位整数
  • float、double:单双精度浮点数
  • bool、char:布尔类型、字符类型
  • decimal:小数类型的有效数字精度为28位

10. 格式化字符串

  • C#中格式化字符串不使用%d,而是用花括号+数字(替代标记)

11. 访问修饰符

  • private:私有,不写默认为私有
  • public :公有
  • protected:受保护的
  • internal:内部的,同一程序集内可以访问
  • protected internal:内部的或受保护的

12. ref和out

  • 使用ref时,相当于传入指针或引用
    • 对于引用类型的数据,ref相当于二级指针
    • 如果不使用ref,在方法内部使用new时,不会同步到方法外,但是对象的内容会被修改
    • 如果使用ref,对象的内容会被修改,同时引用的执行也可以被更改
  • 和ref不一样,ref对应引用,而out是输出返回用的
    • 如果是out,则在函数内部不能读取到原来的值,使用前必须先给其赋值
    • 在调用函数时可以没有初始值,而且初始值对方法来说无所谓因为在内部会被先修改

13. 命名参数

  • 可以在调用函数时,通过指定形参名的方式更改传参的顺序

  • 虽然看上去没什么用,所以这里举一个例子

  • static void MyFunc(int a, int b = 456, int c = 789) {
        System.Console.WriteLine("{0}, {1}, {2}, ", a, b, c);
    }
    static void Main(string[] args) {
        int[] nums = new int []{1, 2, 3};
        MyFunc(123, c: 2);
    }   
    

14. 可选参数要放在参数数组即params前

15. 类中出了字段、方法以外还有属性,属于函数成员(执行代码)

16. 对象初始化语句

  • new MyCalss {x = 10, y = 20};
    new MyCalss(10, 20) {x = 10, y = 20};
    
  • 只能对public的成员使用

  • 执行晚于构造函数

  • 花括号内不加分号,花括号结束后要加毕竟是一条语句

17. readonly修饰符

  • 与const的区别:
    • readonly可以在初始化时或构造函数内赋值,如果是static则要在静态构造函数里赋值
    • const在编译时决定,readonly在运行时决定,可以在不同情况下使用不同的值
    • const的行为是静态的不能用static修饰,但readonly可以是实例字段也可以是静态字段

18. 访问器

  • 默认情况下get和set和对应的属性/索引器有相同访问级别
  • 可以给get和set分别添加访问级别,如get; private set;
  • 只有get和set两个都出现时才能添加访问修饰符,而且只能有一个访问器能有访问修饰符
  • 访问器的访问权限需要比对应的成员更加严格

19. 分部类

  • 一个类拆成多个类来写,每一部分类名相同,都在前面加上partial修饰符
  • 在WPF中,每个页面都会生成两个类文件,一个声明页面上的控件,一个可用于实现页面或表单组件的外观和行为

20. 分部方法

  • 一个分部类给出声明,另一个分部类给出实现
  • 两部分可以在同一个分部类中,也可以放在不同的分部类中
  • 条件:
    • 返回类型必须是void
    • 方法的签名不包括访问修饰符,即隐式私有的
    • 参数列表不能有out
    • 两部分方法前都加上partial修饰符
  • 如果要用分部方法
    • 可以有声明没有实现,这种情况下编译器会把方法的声明和方法内所有对方法的调用删除
    • 不能只有实现没有声明

21. 继承方法和父类方法同名时

  • 不加关键词为直接覆盖,不推荐,会报警告
  • 加关键词new为覆盖/屏蔽,此时如果用父类引用指向子类对象时,执行方法为父类方法
  • 加关键词override为重写/覆写,此时如果用父类引用指向子类对象时,执行方法为子类方法
    • 注意只有父类声明为virtual或abstract时,子类才允许重写

22. 构造函数初始化语句

  • base:调用基类构造

  • class A {
        int x;
        A(int x = 0) {
            this.x = x;
        }
    }
    class B : A {
    	int y;
        public B() : base() { }	// 等价于 public B() { }
        public B(int x, int y) : base(x) {
            this.y = y;
        }
    }
    
  • this:提取当前类构造函数的公共部分

  • class Stu {
        readonly int id;
        readonly string name;
        float score;
        int x;
        private Stu(string name, int id) {
            this.name = name;
            this.id = id;
        }
        public Stu(string name, int id, float score) : this(name, id) {
            this.score = score;
        }
        public Stu(string name, int id, int x) : this(name, id) {
            this.x = x;
        }
    }
    

23. 类访问修饰符

  • public:可以被任何程序集中的代码访问
  • internal:只能被自己所在的程序集内的类看到(类的默认访问级别)

24. 成员访问修饰符

  • public:任意程序集均可访问
  • private:仅自身类可以访问
  • protected:仅自身类以及子类可以访问
  • internal:相同程序集中的类均可访问
  • protected internal:相同程序集中可访问,其他程序集中继承自该类的类可访问,即protected和internal是相或的关系

25. 抽象类和抽象成员

  • 抽象成员
    • 只有函数成员可以声明为abstract,方法和访问器只写声明不写实现,加分号
    • 抽象类中的方法、属性、事件、索引器可以声明为abstract
    • 可以被继承重写且非抽象类中必须重写,但是不用而且不能声明为virtual,重写时使用override
  • 抽象类
    • 用abstract修饰,不能实例化
    • 可以包含抽象或非抽象的成员
    • 抽象类继承抽象类可以不用重写抽象成员;非抽象类继承自抽象类必须重写所有抽象成员

26. 密封类

  • 被sealed修饰的类为密封类,密封类不能被继承
  • 一个类不能同时被abstract和sealed修饰,因为一个要求必须继承重写另一个不能被继承
  • 类似Java中的final类

27. 静态类

  • 类本身需要被标记为static
  • 成员必须是静态的,声明为static或const
  • 可以有静态构造函数,但是不能实例化
  • 不能被继承,是密封类(隐式密封类,不需要声明为sealed)
  • 可以通过类名访问成员名
  • 常用的Math类就是静态类

28. 扩展方法

  • 通过在静态类中的公开静态方法中的第一个参数添加this,可以通过点去调用

  • public static class ExtendString {	// static
        public static int Find(this string s, string t) {	// public static	
            for (int i = 0; i < s.Length; i++) {
                if (s[i] == t[0]) {
                    bool f = true;
                    for (int j = 0; j < t.Length && i + j < s.Length; j++) {
                        if (s[i + j] != t[j]) {
                            f = false;  break;
                        }
                    }
                    if (f)
                        return i;
                    }
                }
            }
            return -1;
        }
    }
    
    class Program {
        static void Main(string[] args) {
            string s = "123", t = "22";
            int id = s.Find(t);	// s.Find(t);
            System.Console.WriteLine(id);
        }      
    }
    

29. 浮点数取余

  • 在C#中可以对浮点数进行取余
  • 运算规则:如2.0 % 1.5 = 0.5,2.5 % 1.5 = 1等

30. 类型转换的运算符重载

  • class LimitedInt {
        readonly int maxValue;
        readonly int minValue;
        private int value;
        public int Value {
            get {
                return value;
            } 
            set {
                this.value = value;
            }
        }
        public LimitedInt(int x, int max = int.MaxValue, int min = int.MinValue) {
            Value = x;
            maxValue = max;
            minValue = min;
        }
        // implicit为隐式转换,explicit为显式转换
        public static implicit operator int(LimitedInt limitedInt) {	// LimitedInt转换为int
            return limitedInt.Value;
        }
        public static implicit operator LimitedInt(int x) {		// int转换为LimitedInt
            return new LimitedInt(x);
        }
    }
    
    class Program {
        static void Main(string[] args) {
            LimitedInt limitedInt = 100;
            int x = limitedInt;
            System.Console.WriteLine(x);
        }  
    }
    
  • 如果声明了implicit隐式转换,也可以使用强制转换

  • 但是如果只声明了explicit显式转换,则不能使用隐式转换,必须使用显式转换(强制转换)

  • 两者不能同时声明

31. using语句

  • 使用命名空间是using指令,不是using语句

  • 资源是指实现了IDisposable接口的类或结构

    • 不适用using的情况下释放资源可以使用

    • Type t = new Type();	// 分配资源,其中Type必须实现IDisposable
      // 使用资源
      t.Dispose();	// 处置资源
      
    • 但是如果在使用资源的时候发生了异常可能导致Dispose不会被调用,需要手动处理异常

  • using语句可以尽快释放资源并确保这些资源被适当处置

    • using (Type t = new Type()) {	// 分配资源,其中Type必须实现IDisposable
          // 使用资源
      }	// 处置资源(隐式)
      
    • using会执行以下内容:

      • 分配资源
      • 把使用资源的部分放入try块中
      • 创建资源的Dispose方法的调用,并放进finally块中
    • 这样就方便了程序员不需要手动处理异常,还能确保资源得到适当的处置

    • using的小括号中可以用分配多个资源并用逗号隔开,并且using语句可以嵌套使用

  • using还有另外一种形式

    • Type t = new Type();	// 或其他分配资源的方式
      using (t) {
          // ... 
      }
      
    • 即把资源的声明和分配放在using之前

    • 不推荐使用,因为和另一种形式相比,不能保证在使用using之前资源没有被释放,或者在using执行结束后资源被释放后继续使用资源

  • using一般使用于一些如数量有限制或耗费系统资源的情况,如文件的读写、数据库的连接等

32. StringBuilder

  • 位于System.Text命名空间
  • 是Unicode字符的可变数组

33. 把字符串解析为数据值

  • 目标类型.Parse方法,成功返回数据值,失败抛出异常
  • TryParse方法,返回布尔值是否成功,通过out参数返回数据值给输出变量

34. 可空类型

  • 可空类型总是基于基础类型
    • 可以从任何值类型创建可空类型
    • 不能从引用类型或其他可空类型创建可空类型
    • 不能在代码中显式声明可空类型,只能声明可空类型的变量
    • 编译器会使用泛型隐式创建可空类型
  • 声明时添加一个?后缀到类型后面
  • 可空类型的结构
    • 基础类型的实例
    • 几个重要的只读属性
      • HasValue:bool类型,指示值是否有效
      • Value:和基础类型相同的类型并且返回变量的值
  • 如果不是null可以和其他类型的变量一样使用
    • 可以使用HasValue或和null比较来检测是否有值
  • 可空类型和普通类型之间的转换
    • 普通类型到可空类型:隐式转换
    • 可空类型到普通类型:显式转换
  • 为可空类型赋值
    • 基础类型的值
    • 同一可空类型的值
    • null
  • 空接合运算符
    • x ?? y
      • 如果x为空,返回y的值
      • 如果x不为空,返回x
    • 两个空值被认为相等
  • 使用可空用户自定义类型
    • 结构的可空形式只能通过Value属性访问内部基础类型,不直接暴露任何成员,即便是公共的
  • Nullable<T>
    • 可空类型通过一个叫做System.Nullable<T>的.NET类型来实现
    • 可空类型的问号语法是创建Nullable类型变量的快捷语法

35. 嵌套类型

  • 在类或结构中声明类型,这种类型叫做嵌套类型
  • 嵌套类型像封闭类型的成员一样声明
    • 嵌套类型可以是任意类型
    • 嵌套类型可以是类或结构
  • 如果一个类只作为帮助方法并且只对封闭类型有意义,可能需要声明为嵌套类型
  • 嵌套类型的对象不一定封闭在封闭类型的对象之内,嵌套类型的对象和它没有在另一个类型中声明时所在的位置一样
    • 也就是说嵌套类型的对象不是封闭类型的成员,封闭类型对象中只有引用
  • 可见性和嵌套类型
    • 嵌套类型有成员访问级别,而不是类型访问级别
      • 在类内部声明的嵌套类型可以有5种成员访问级别的一种:public、protected、private、internal、protected internal
      • 在结构内部声明的嵌套类型可以有3种成员访问级别的一种:public、internal、private
    • 嵌套类型的默认访问级别是private
    • 嵌套类型的对象可以访问封闭类型的所有成员,不管是不是public
    • 封闭类型的成员总是可以访问封闭类型本身和有访问权限的嵌套类型成员
  • 嵌套类型的可见性还会影响基类成员的继承
    • 如果封闭类型是一个派生类,嵌套类型就可以使用相同的名字来隐藏基类成员
  • 嵌套类型中的this引用指向的是嵌套类型的对象
    • 如果嵌套类型的对象需要访问封闭类型,必须持有封闭类型的引用
    • 可以把封闭对象提供的this引用作为参数提供给嵌套类型的构造函数

36. 析构模式和dispose模式

  • C#中类可以有析构函数
    • 非托管资源指类似用Win32API或非托管内存块获取的文件句柄这样的资源,使用.NET资源无法获取它们,因此如果只使用.NET类是不需要编写太多析构函数的
    • 析构函数(在C#3.0之前有时也称为终结器)
      • 每个类只能有一个析构函数
      • 析构函数不能有参数
      • 析构函数不能有访问修饰符
      • 析构函数名称与类名相同,但是前面要加一个~
      • 没有静态析构函数
      • 不能显式调用,当垃圾回收器分析代码并认为不存在指向该对象的可能路径时,系统会在垃圾回收的过程中调用析构
    • 原则
      • 不要在不需要时实现析构函数,会严重影响性能
      • 析构函数应该只释放对象拥有的外部资源
      • 析构函数不应该访问其他对象,因为无法认定这些对象是否已经被销毁
    • 构造函数和析构函数
      • 实例构造函数:创建类的每个实例时调用一次
      • 静态构造函数:只调用一次,即首次访问任意静态成员之前,或创建类的任意实例之前
      • 析构函数:类的每个实例在程序流不再访问该实例之后的某个时刻调用
    • 析构函数调用时机
      • 与C++不同,C#析构函数不会在超出作用域时立即调用,事实上无法知道何时会调用析构函数
      • 也不能显式调用析构,只能知道系统会在对象从托管堆上移除之前的某个时刻调用析构函数
      • 如果要求非托管资源越快释放越好,则不能使用析构函数,而应该使用dispose模式
  • 标准dispose模式
    • 特点

      • 包含非托管资源的类应该实现IDisposable接口,后者包含单一方法Dispose,Dispose包含释放资源的清除代码
      • 如果代码使用完了这些资源并且希望系统将它们释放,应该在程序代码中调用Dispose方法,注意是手动调用而不是系统去调用
      • 类还应该实现一个析构函数,在其中调用Dispose方法,以防止之前没有调用该方法
    • 析构函数和Dispose代码应该遵循以下原则

      • 如果由于某种原因代码没有调用Dispose,那么析构函数应该调用它并释放资源
      • 在Dispose方法最后应该调用GC.SuppressFinalize方法,通过CLR不要调用该对象的析构函数
      • 在Dispose中实现这些代码,这样多次调用该方法是安全的,重复调用时,后续调用不应该执行任何额外的操作,也不应该抛出任何异常
    • 标注的dispose模式

      • Dispose方法提供两个重载,一个是public,一个是protected,protected的重载包含实际的清除代码

      • public版本在代码中显式调用以执行清除工作,他会调用portected版本

      • 析构函数调用protected版本

      • protected版本的bool参数通知方法是被析构函数或是其他代码调用

      • class Test : IDisposable {
            bool disposed = false;
        
            public void Dispose() {
                Dispose(true);
                GC.SuppressFinalize(this);	// Dispose后CLR不调用析构
            }
            ~Test() {
                Dispose(false);	// 如果没有调用Dispose保证会被析构调用
            }
        
            protected virtual void Dispose(bool disposing) {
                if (disposed)   return ;	// 多次调用是安全的
                if (disposing) {
                    // 释放托管资源
                }
                // 释放非托管资源
                disposed = true;
            }
        }