C#笔记 其一

类的声明和实例化

class A{}

// another file
F(){
    A a = new A(); // 实例化类,new <类名>(实例化参数)
    A aa;
    aa = new A(); // 先声明后实例化同样可行
}

类的成员

字段和方法
class A {
    private string Name;
    protected int Seq;
    internal int Abc;
    public int Score;
    
    public void Foo(int x) {}
}

class B {
    public static int Bef;
    public static int Bar() {}
}

F(){
    A a = new A(); // A中成员和方法都是非静态的,所以先创建一个A的实例
    System.Console.WriteLine(a.score)

    System.Console.WriteLine(B.b); // B中成员和方法都是静态的,所以通过类调用
}
属性
class A {
    private string _Name; // 当综合使用private字段和public属性时
    public string Name { // 用下划线加PascalCase为private字段命名,用PascalCase为public属性命名
        get{ return _Name; }
        set{ _Name = value; }
    }
}

class B {
    private string _Name;
    public string Name{
        get => _Name;
        set => _Name = value; // 简化主体式表达方法
    }
    public int Seq { get; set; } // 使用自动实现的属性方法
}

在对属性使用或赋值时进行操作

class A {
    private string _Name;
    public string Name {
        get => _Name;
        set {
            if (value == null)
                throw new ArgumentNullException(nameof(value));
            else if (value == "")
                throw new ArgumentException("Value can't be empty", nameof(value));
            else
                _Name = value;
        } // 对属性赋值进行检查
    }
    public int Seq {
        get;
        private set; // 把属性的方法声明为private就可以覆盖属性的public,这样只有本类才能使用set
        // 注意:set只能比get更私有,若如果set为public,get为private则出错
    }
    public string Info { // 虚属性
        get => $"{Seq} {Name}";
        set {
            string[] info = value.Split(new char[] { ' ' });
            if (info.Length == 2 && int.TryParse(info[0], out int seq))
            { // not `out Seq` 属性不作out和ref的参数
                Name = info[1];
                Seq = seq;
            }
            else
                throw new ArgumentException($"{value} is invalid");
        }
    }
    public int Score {
        get; // 只定义了get,则为只读属性
    }
    public bool pass {get; set; } = false; // 初始化属性
}

自动实现的属性代替public,protected字段

注意不要出现如下代码

class A {
    public string Name {
        get; // Error
        set => Name = value; // 错误:死循环
    }
}
关键字this
class A {
    private string Name;
    private int Seq;
    public void SetA(string name, int seq)
    {
        this.Name = name;
        this.Seq = seq;
        // this作为对自身的引用
    }
}

没有必要不使用this

构造函数和解构函数

class A {
    public string Name { get; set; }
    public int Seq { get; set; } = -1;
    public int Score { get; set; }
    public bool Pass { get; set; }
    public A(string name, int seq) { // 构造函数名和类名一样
        Name = name;
        Seq = seq;
    }
    public A(string name) => Name = name; // 重载构造函数,考虑为构造函数添加默认值而非简单重构
    public A() :
        this(string.Empty, -1) // 构造函数链,在C++中称为委托构造函数
    {
        Score = 59;
    }

    public void Deconstruct( // 解构函数,必须为void Deconstruct (out type typename) 形式
        out string name,
        out int seq,
        out int score,
        out bool pass
    )
    {
        (name, seq, score, pass) = (Name, Seq, Score, Pass);
    }
}

void Foo() {
    A a = new A("Alice", 12); // 调用构造函数,我们给Seq的默认值-1会被覆盖
    // new 获取空白内存,传递给构造函数进行实例化,再返回内存的引用
    A aa = new A("Bob", 45) { Score = 60, Pass = true };
    // 用初始化列表对可访问属性进行初始化
    // 实际上是语法糖,与分别赋值等同
    var aaa = new System.Collections.Generic.List<A>() {
        new A("Ted", 78),
        new A("Carol", 32)
    }; // 集合初始化器

    a.Deconstruct(out string name, out int seq, out int score, out bool pass); // 直接调用解构函数
    var (name, _, score, pass) = a; // 元组语法
}

封装修饰符

封装
  • 组合
    面向对象编程可以将相关联的属性和方法结合在一个类中

    void Foo(int seq, string name) {} // 传递多个参数
    void Foo(A a) {} // 传递一个类
    

    相信先接触 struct 结构体类型的人应该更深地体会到将多个属性放在一个 struct 中带来的方便之处,而 class 不仅组合了属性,还有与之相关的多种方法。现在面向对象编程非常的成熟,众多高级语言都要使用到面向对象编程的思想。

  • 隐藏
    C语言中可以通过 static 关键词使变量和函数仅在当前文件中可被访问,通过 {int a}; /* can find a */ ,作用域也是隐藏变量的手段之一。而在面向对象编程的实现中,一个 class 就是一个单独的作用域,还有上述的 private, public 等来控制成员在类外的可见性
    我们可以自由控制私有程度来隐藏类中的数据和实现细节,减少类的使用者对其的不恰当修改和破坏,只要提供给使用者一个接口就足够了。同时,只要接口不变,类的制作者进行修改后,使用者也不用修改自己的代码。

静态

静态字段、属性和方法属于类和全体实例

public class A {
    public static int Count { get; private set; } // 计算A的实例化次数,静态字段会被默认初始化
    public int Seq { get; set; }
    A() => Seq = ++Count;
    
    static A(/* 无参数 */) { // 静态构造函数,是对类的构造而非对实例的构造,在第一次使用类的时候被调用
        Count = new System.Random().Next(0, 100); // 一般用于对静态成员的复杂初始化
    } // 如果在此时抛出异常会使得A无法被构造而无法使用
}

public static class B { // 静态类
    // 无法实例化
    // 无实例字段和方法
    // static类同时也是abstract和sealed,无法派生
    // 可以使用using static引入静态类 using static System.Console

    public static void Foo(this A a) => System.Console.Write(a.Seq);
    // 在静态类中为其他类添加实例方法,在类型前加 this 关键字
    // Foo的私有等级要比A高
    
    static void F() {
        a.Foo(); // A没有实例方法Foo,但我们在B中添加了
    }
}
字段修饰符
class A {
    const float Pi = 3.14159F; // const字段自动成为static字段
    // const字段只能用于short, int, long, double等拥有字面值的数据类型
    
    public readonly int Seq = 12; // 可以在类中指定初始值
    public A (int seq) {
        Seq = seq; // 只能在构造函数中更改
    }
    // readonly只用于字段(const可用于局部变量)
    // readonly可以修饰非字面值的字段
    private readonly System.Random rd = new System.Random();
    // readonly -> read-only 由于只读属性的出现,readonly的使用大大减少了
}
访问权限修饰符
修饰符 本类 派生类 程序集 其他类 描述
private $$\surd$$
protected $$\surd$$ $$\surd$$ 派生类型
private protected $$\surd$$ $$\circ$$ $$\circ$$ 同程序集 且 派生类型
internal $$\surd$$ $$\circ$$ $$\surd$$ $$\circ$$ 同程序集
protected internal $$\surd$$ $$\surd$$ $$\surd$$ $$\circ$$ 同程序集 或 派生类型
public $$\surd$$ $$\surd$$ $$\surd$$ $$\surd$$

派生类不能访问基类的 private 成员,除非派生类同时是基类的嵌套类

Assembly被翻译为“程序集”、“组合体”、“装配件”、“配件”等

类的修饰符只有 publicinternal ,默认为 internal
修饰符主要用于成员

派生类访问基类的 protected 成员时,不能通过基类实例访问

class Base {
    protected int N;
    static protected int M;
}

class Derived: Base {
    public void Foo() => System.Console.Write(N); // CORRECT
    public void Bar() => System.Console.Write(Base.M); // CORRECT
    public void Baz(Base b) => System.Console.Write(b.N); // ERROR
}

特殊类

嵌套类
class A {
    private class B {
        public Name;
        public Seq;
        private Aaa; // A无法访问private修饰的Aaa,但B可以访问A的所有成员
        
        public void Foo(A a) { return a.N; } // 传递A的实例,B可以访问A的所有成员(无论private)
    }
    private int N = 54;
}
分布类和分部方法
// File: first.cs
partial class A {
    partial void Foo(string s); //分布方法必须返回void,但可以用ref参数返回值
}
// File: second.cs
partial class A {
    partial void Foo(string s) {
        System.Console.WriteLine(s);
    }
}
// 可用于将A中的嵌套类B分开

继承

派生
public class Base {
    public string Name { get; set; }
}

public class Derived: Base {
    public int Seq { get; set; }
}

void F() {
    Derived d = new Derived();
    System.Console.Write(d.Name);
    // 每个派生类都有基类的所有成员,在继承链上也是如此
    
    Baes b = d; // 派生类可以隐式地转化为基类
    Derived dd = (Derived)b; // 基类必须显式地转化为派生类,但这有可能失败,确保你知道这么做的结果
    
    // 注意:此时b是d的引用
    b.Name = "abc";
    System.Console.Write(d.Name); // display: abc
    System.Console.Write(ReferenceEquals(a, b)); // True
    
    // 事实上
    Derived e = d;
    System.Console.Write(ReferenceEquals(e, b)); // True
}

C#的单继承模式是其区别于C++的一个方面

转换
int i = 32;
double d = i; // 隐式
long l = 123;
i = (int)l; // 显式

class A {
    public static explicit operator B(A a) { /* transform A into B */ } // 自定义显式转换
}

class B {
    public static implicit operator A(B b) { /* transform B into A */ } // 自定义隐式转换
}
密封
public sealed class A { } // sealed使A无法被派生
public class Base {
    public virtual void F() { }
}
public class Derived: Base {
    public override sealed void F() { } // 使用sealed使该方法不能再被重写
}

string就是一个密封类

对比C++的 final

is操作符
string a = string.Empty;
System.Console.WriteLine(a is string); // True
System.Console.WriteLine(a is object); // True object是string的基类
System.Console.WriteLine(a is null); // False

object o = a;
System.Console.WriteLine(o is string); // True 装箱后依旧可以识别
System.Console.WriteLine(o is object); // True

class A { }
class B : A { }
B b = new B();
A a = b;
Write(a is B); // True


if (o is string) {
    string s = (string)o; // 拆箱
    // DoSometing
}
else if (o == null) {
    // DoSomething
}
// 可以简写成如下形式
if (o is string s) {
    // DoSometing
}
else if (o is null) {
    // DoSomething
}
as操作符
class A { }
class B : A { }
void F(B b) { System.Console.Write(b == null ? "null" : "B"); }

A a;
F(a as B); // 类型转换,若失败,返回null
F((B)a); // Error

基类

虚函数
public class A {
    protected int _N;
    public virtual int N { // 虚属性和虚方法不能是private
        get => _N;
        set => _N = value;
    }
}

public class B: A {
    public override int N {
        get => _N;
        set => _N = value * 2;
    }
}

public class C: A {
    public new int N {
        get => _N;
        set => _N = value * 3;
    }
}

public class D: B { // 继承B
    public new int N {
        get => _N;
        set => _N = value * 4;
    }
}

如果B不想使用A中N属性的定义,为了重写N属性,A显式使用 virtual 将一个函数修饰为虚函数,B使用现实 override 重写N定义,C使用 new 覆盖N定义(允许重写属性和方法不允许重写字段和静态成员,允许覆盖所有成员)

重写和覆盖

overridenew 的区别

void F() {
    B b = new B();
    C c = new C();
    D d = new D();
    
    A a = b; // a是b的引用
    a.N = 10;
    System.Console.WriteLine(b.N); // display: 20
    
    a = c; // a是c的引用
    a.N = 10;
    System.Console.WriteLine(c.N); // display: 10

    a = d; // a是d的引用
    a.N = 10;
    System.Console.WriteLine(c.N); // display: 20
}

使用 override 调用的是重写链最末端的方法,使用 new 相当于截断了重写链

  • A到B,末端为B,调用B的方法
  • A到C,C处被截断,所以末端为A,调用A的方法
  • A到D,D处被截断,所以末端为B(D继承B),调用B的方法

virtualnew 可以一起使用

void F() {
    C c = new C();
    B b = c;
    b.F(); // display: 789
}

class A { public void F() => System.Console.Write("123"); }
class B : A { public virtual new void F() => System.Console.Write("456"); }
class C : B { public override void F() => System.Console.Write("789"); }
关键字base
class A {
    public virtual string F() => "123";
    public string G() => "789";
    private int _N;
    public int N { get => _N; set => _N = value; }
    public A(int n) { _N = n; }
}
class B: A {
    public override string F() => base.F() + base.G(); // 调用基类方法
    public B(int n) : base(n) { } // 调用构造函数
}
抽象基类

抽象类对应具体类

public abstract class A {
    public int N;
    public abstract string GetS(); // 抽象成员(属性或方法)
}

public class B: A {
    public override string GetS() => "123"; // 必须实现抽象成员
}

void F() {
    A a = new A(); // 错误:抽象类无法被实例化
    System.Console.Write(new B().GetS());
}

abstract 成员是自动 virtual

对比C++的 = 0 来定义抽象函数:

  • 可以在类外定义(可以有主体部分)
  • 类不用再进行虚类声明
System.Object类

object 是所有类的基类,定义了如下方法

FuncName Explanation
public virtual bool Equals(object o) 参数和当前对象值相同与否
public virtual int GetHashCode() 对象的Hash值
public Type GetTyep() 对象对应类型的字符串
public static bool ReferenceEquals(object a, object b) 是否表示同一个对象(地址)
public virtual string ToString() 对象实例的字符串表示
public virtual void Finalize() 析构器的别名,无法直接调用
protected object MemberwiseClone() 浅拷贝

接口

实现接口

接口是对类的实现的一种契约,实现接口的类必须实现接口所要求的属性和方法,而类的使用者只需要使用接口就可以了

创建接口
interface IFirst {
    void Greet(string args);
    string Name { get; set; }
    // 接口中声明方法和属性(不能声明字段)
}
interface ISecond {
    void Print();
    int Seq { get; }
}
显式和隐式实现接口
class A: IFirst, ISecond { // 实现多个接口
    // 实现主体
    void IFrist.Greet(string args) {} // 显式实现必须加接口名,不加修饰符
    public string Name { get; set; } // 隐式实现
    // 隐式实现要求成员是public的
    // 隐式实现的成员是virtual或sealed的(默认sealed)
    
    // ISecond的实现
}

void F() {
    A a = new A();
    System.Console.Write(a.Name); // 隐式实现成员
    ((IFirst)a).Greet(string.Empty); // 显式调用必须进行强制转换
}

决定显式还是隐式:

  • 成员是不是类的核心功能?隐式:显式
  • 接口成员的名称是否恰当?隐式:显式
  • 是否有同名成员?隐式:显式
抽象类实现接口
abstract class B: IFirst, ISecond {
    public abstract void Greet(); // 抽象方法实现接口
    public void Print() => System.Console.Write(123); // 不用abstract
    // ...
}

接口扩展

接口和类的转换
void F(IFirst a) { /* */ } // 实现了IFirst接口的类的实例作参数

实现类型到接口的转换总是成功(隐式转换),但反之可能失败(需要显式转换)

接口继承
interface IFirst { void F(); }
interface ISecond: IFirst { void G(); }

class A: ISecond /* , IFirst */ {
    void IFirst.F() /* not ISecond.F() */ { /* */ } // 显式实现需要用最初声明该方法的接口
    void ISecond.G() { /* */ }
}

接口也支持多接口继承

当想要更改接口时,不要直接修改当前接口(会引起使用接口成员的函数和类失效),而是通过继承创建新的接口

接口的扩展方法
void F(this IFirst[] args) { /* */ }

// 用此方法实现“多继承”
static class M { public static void Foo(this I a) { System.Console.Write(12); } }
    // 为接口实现扩展方法,这样每个实现类都有这个方法
interface I { }
class A: I { }
class B: I { }

void F() {
    new A().Foo(); // display: 12
}

抽象类和接口的比较

抽象类 接口
不能直接实例化,只能创建派生类 不能直接实例化,只能创建实现类
派生类要么也是抽象类,要么把所有抽象方法实现 实现类必须实现所有接口成员
可以添加非抽象成员 不直接添加,要通过继承来添加成员,否则会导致版本破坏
可以声明字段、属性和方法(包括构造函数和终结器) 只能声明属性和方法
成员可以是 virtual abstract static
非抽象成员可以提供默认实现
成员都是默认 public sealed
不能声明静态成员,所有成员都是自动抽象的
只能继承一个基类 可以多接口继承

抽象类是类,可以表示一种概念,是派生类的抽象本质(is something)
接口是契约,约定了类的实现和使用方法,是与外界交互的通道(can do something)

常用接口

名称 描述
IComparable 如果一个类要实现与其它对象的比较,则必须实现IComparable接口。
由可以排序的类型,例如值类型实现以创建适合排序等目的类型特定的比较方法。
IComparer 是特定用于Array的Sort和BinarySearch方法,
通过实现IComparer接口的Compare方法以确定Sort如何进行对对象进行排序
IEnumerable IEnumerable接口公开枚举数,该枚举数支持在集合上进行简单迭代。
IEnumerable接口可由支持迭代内容对象的类实现。
IEnumerator IEnumerator接口支持在集合上进行简单迭代。是所有枚举数的基接口。
枚举数只允许读取集合中的数据,枚举数无法用于修改基础集合。
ICollection ICollection接口定义所有集合的大小、枚举数和同步方法。
ICollection接口是System.Collections命名空间中类的基接口。
IDictionary IDictionary接口是基于ICollection接口的更专用的接口。
IDictionary 实现是键/值对的集合,如Hashtable类。
IList IList接口实现是可被排序且可按照索引访问其成员的值的集合,如ArrayList类。

NET Framework 2.0 以上版本的.net framework提供了响应泛型的接口,如IComparable、IEnumerable、IEnumerator、ICollection、IDictionary和IList等泛型接口的功能与非泛型接口的功能一样,但适用于更多的类,性能方面要高于非泛型接口,因此建议能使用泛型接口的都使用泛型接口实现

值类型

值类型和引用类型

特性 值类型 引用类型
内存存储位置 存储在栈(stack)上 存储在堆(heap)上
拷贝策略 值类型变量之间复制时,
会拷贝出一个新的值(带 out ref 的值除外),
更改其中一者不影响另一个
引用类型拷贝的是地址
两者所引用的其实是同一个值,
改变其中一个会导致另一个所引用的值也改变
访问方式 直接包含数据 指向数据的指针
需要经过一次跳转才能访问到真正的变量
内存需求 较少 较多
执行效率 较快 较慢
内存回收 离开变量作用的定义域时 在所有引用消失后由垃圾回收器(GC)回收
声明和使用结构
public readonly struct M // 在struct前加readonly表明只读
{
    // 不能声明M()默认构造函数
    public M(int hours, int minutes, int seconds)
    {
        Hours = (hours + (minutes + seconds / 60) / 60) % 24;
        Minutes = (minutes + seconds / 60) % 60;
        Seconds = seconds % 60;
    }
    public M(int hours, int minutes) : this(hours, minutes, default(int)) { }
    public M(int hours) : this(hours, default(int), default(int)) { }

    public int Hours { get; } // 因为readonly所以不能实现set
    public int Minutes { get; }
    public int Seconds { get; }
    public M Move(int hours, int minutes, int seconds) =>
        new M(Hours + hours, Minutes + minutes, Seconds + seconds);
    public override string ToString() =>
        $"{Hours}:{Minutes}:{Seconds}";
} // 结构中的数据在使用new时调用构造函数显式初始化,使用数组时隐式初始化为默认值(default)

void F() {
    M a = new M(600, 22, 45);
    M b = new M();
}

建议在 struct 类型前加上 readonly 修饰符使自定义值类型只读(就像内置的值类型)

对比C++的struct和class

继承和接口

enum 的值类型总是派生自 System.ValueType , 而 System.ValueType 派生自 object

结构也能实现接口
如果希望比较相等性,应该重写 Equal(), ==, != 等,并考虑实现 IEquatable<T> 接口

装箱和拆箱
object ob = new M(12);
System.Console.WriteLine($"{ob}"); // 重写了 ToString()
ob = "8465"; // 可存储不同类型
System.Console.WriteLine(ob);

M val = new M(12,45,78);
object obj = val; // boxing
val = (M)obj; // unboxing

值类型的装箱和拆箱需要注意

// 如果此时struct M是可变的
M m = new M(12,5,6);
object ob = m;
((M)ob).Hours += 5;
// 此时m的值没有改变

// 考虑如下相似情况
int val = 12;
ob = val; // ob装的是val
((int)ob) += 4; // 但是拆箱出的是被拷贝的一个临时值(根据值类型的定义),与val无关的值
// 事实上,上述代码是会报错的,因为int是readonly的

这就是我们为什么要将值类型声明为 readonly 的原因之一了

枚举

声明和使用枚举
enum State: short /* 指定除char外的基础类型,默认为int */ {
    None,
    On,
    Off = 10, // 可以显式地为枚举赋值
    Error, // ERROR = 11
    Down = Error
}

void F() {
    System.Console.Write(State.On);
    System.Console.Write((int)State.Down);
}
类型转换
State a = (State)1; // 转换整型数字
State b = (State)Enum.Parse(typeof(State), "On"); // 转换string
if(Enum.TryParse("On", out c)) {
    System.Console.Write(c);
}

FlagAttribute

如果枚举是可以组合的,如

[Flags]
enum State: short {
    None = 0,
    On = 1 << 0,
    Off = 1 << 1,
    Error = 1 << 2,
    Down = Error
    // State.Off | State.Down = 6
    // 枚举值可以组合
}

则用 [Flags] 标记

posted @ 2022-10-27 11:51  Violeshnv  阅读(22)  评论(0编辑  收藏  举报