C#笔记 其二

良好的构建

重写基类object成员

ToString()

如果没有重载 ToString() ,那么这个函数返回类型的名称

System.Console.Write(new Random().ToString()); // display: System.Random

但有时候我们需要这个函数输出更有意义的信息

public readonly struct Complex
{
    public Complex(double x) : this(x, default(double)) { }
    public Complex(double x, double y) { RealPart = x; ImagPart = y; }
    public double RealPart { get; }
    public double ImagPart { get; }
    public override string ToString() =>
        $"({RealPart}+{ImagPart}j)";
}

System.Console.Write() System.Console.WriteLine() System.Diagnostics.Trace.Write(a); 等函数都会调用 ToString()

GetHashCode()

如果重写了 Equals() 就要重写 GetHashCode() ,因为如果两个值是相同的,那么它们的Hash值也应该是相同的
如果想将类型作为哈希表的键值,那么也应该重写 GetHashCode()

HashCode具有如下要求和期望:

  • 必要的

    1. 相等的对象具有相等的HashCode,如果 a == b ,则 hash(a) == hash(b)
    2. 在对象的生存期内,对象的HashCode不应发生改变
    3. hash函数不应引发异常
  • 追求的

    1. HashCode应该在值域内均匀分布

    2. hash函数具有良好的性能

    3. 越相近的值,HashCode就应该越远离

      hash(1.00) = 1
      hash(1.01) = 23058430092136961
      hash(1.02) = 46116860184273921
      
    4. 使用者(甚至是攻击者)难以从HashCode推出原值

public override int GetHashCode()
{
    return (RealPart, ImagPart).GetHashCode();
} // 使用元组的Hash算法

我想到了一个绝妙的具体的Hash算法,但是这里地方太小了,写不下

Equals()

全称: Object.Equals(object obj)

注意事项:

  • 如果两个对象引用同一实例,那么他们是相等的,可调用 ReferenceEquals() 来判断

    值类型的 ReferenceEquals() 总是返回 false

  • 如果两个对象相等,那么它们的标识数据要相等

步骤:

  1. 对象是值类型还是引用类型?
  2. 检查 null, 检查 ReferenceEquals()
  3. 检查数据类型
  4. 检查具体类型,类型转换
  5. 可能要比较HashCode
  6. 如果基类重写了 Equals() ,就调用 base.Equals()
  7. 比较每一个标识数据
  8. 重写 GetHashCode()
  9. 重写 ==!= 操作符
public override bool Equals(object obj) // 1
{
    if (obj == null) // 2
        return false;
    if (obj.GetType() != this.GetType()) // 3
        return false;
    return Equals((Complex)obj); // 4
}
public bool Equals(Complex other) // 1
{ other.RealPart == RealPart && other.ImagPart == ImagPart; } // 7
// (other.RealPart, other.ImagPart) == (RealPart, ImagPart); // 元组

重载操作符

x.y f(x) new typeof default checked unchecked delegate is as = => 之外其他所有操作符都支持重载

比较操作符
public sealed class A {
    public static operator ==(A left, A right) {
        if (ReferenceEquals(left, null)) // 使用ReferenceEquals()比较,不要使用 left == null
            return ReferenceEquals(right, null);
        return left.Equals(right); // 假定已实现Equals()
    }
    public static operator !=(A left, A right) {
        return !left==right;
    }
} 
二元操作符
public static Complex operator +(Complex x, Complex y) =>
    new Complex(x.RealPart + y.RealPart, x.ImagPart + y.ImagPart);
public static Complex operator -(Complex x, Complex y) =>
    new Complex(x.RealPart - y.RealPart, x.ImagPart - y.ImagPart);
一元操作符
public static Complex operator +(Complex self) =>
    self;
public static Complex operator -(Complex self) =>
    new Complex(-self.RealPart, -self.ImagPart);
二元赋值复合操作符

重载了二元操作符,就相当于重载了二元赋值复合操作符

条件逻辑操作符和逻辑操作符

条件逻辑操作符不能显式重载,但可以通过重载逻辑操作符和 true/false 操作符来间接实现

true/false 操作符
public static bool operator true(Complex self) =>
    self.RealPart != 0.0 || self.ImagPart != 0.0;
public static bool operator false(Complex self) =>
    self.RealPart == 0.0 && self.ImagPart == 0.0;

在条件判断语句中使用

转型操作符
public static implicit operator Complex(double x) =>
    new Complex(x);
public static explicit operator double(Complex self) {
    if (self.ImagPart != 0.0)
        throw new InvalidCastException();
    else
        return self.RealPart;
}

implicit 表示隐式转换
explicit 表示显式转换

会引发异常的类型转换应该是显式的
会丢失数据的转换应该是显式的,甚至是引发异常的
双向的转换建议一边显式,一边隐式

资源清理和垃圾回收

垃圾回收时运行时(runtime)的主要工作之一,旨在回收不再被引用的对象所占用的内存,但这个任务的关键之处在于运行时只会回收对象的内存而不处理其他资源,如:数据库连接、文件句柄、网络端口和硬件设备等。并且引用一词也表明运行时回收的是堆(heap)上内存,如果对象始终被引用的话就会阻止内存回收

垃圾回收器(Garbage Collection)支持”代“(generation)的概念,新生对象会以更高的频率被尝试清除。在一次垃圾回收中“存活”的对象将会进入下一代,以较低的频率清除。具体地说,总共有三代对象

弱引用
System.WeakReference weakPtr = null;
// bind weakPtr with a FileStream

System.IO.FileStream fp = weakPtr.Target;
if (fp != null) { // 先赋值,后检查
    // DoSomething();
}
else {
    // Load fp
    weakPtr.Target = fp;
}

弱引用不会阻止GC进行垃圾回收

推迟初始化
class A {
    public System.IO.FileStream Fp =>
        InternalFp??(InternalFp = new System.IO.FileStream());
    private System.IO.FileStream InternalFp = null;
}

Fpget 不被调用, InternalFp 永不实例化

终结

终结器
class A {
    public System.IO.FileStream Data { get; set; }
    ~A() { // 无参数,无修饰符
        Data?.Close();
    }
}

终结器只能由垃圾回收器调用。并保证在对象最后一次使用后,程序正常结束(断电、进程强制终止等除外)前被调用,所以编译时不能确定终结器的确切运行时间

确定性终结
class A : IDisposable {
    public System.IO.FileStream Data { get; set; }
    ~A() {
        Dispose(false);
    }
    public void Dispose() {
        Dispose(true);
    }
    public void Dispose(bool flag) {
        if (flag) {
            Data?.Close(); // System.IO.FileStream自带终结器
            System.GC.SuppressFinalize(this); // 从终结队列(f-reachable)中移除
        }
    }
}

实现接口 IDisposable 的方法 Dispose() ,通过直接调用 Dispose() ,就能实现确定性清理资源

有时候在调用 Dispose() 前会引发错误导致 Dispose() 不被调用
此时需要一个 try/finally 结构
但C#提供了更方便的 using 语句

using (A a = new A()) { // A要实现IDisposable
    // DoSomething();
    // Dispose() 会被调用
}
终结队列

System.IO.FileStream 自带终结器,如果我们 A 的终结器被调用,那么表明此时 A 的实例已经在终结队列中了,则 FileStream 的实例也在终结队列中了, FileStream 的终结器或在 A 前或在 A 后被调用。总之,我们没必要(也不应该)再次终结 FileStream

System.GC.SuppressFinalize(this) 的调用,将参数(此处为this)从终结队列(f-reachable)中移除,这是因为此时资源清理已经完成。如果不移除,终结器将会被调用,资源会被清理两次。

当对象从终结队列中被删除(调用终结器或用 System.GC.SuppressFinalize())后,对象才会真正被垃圾回收器回收。带有终结器的类型会被加入终结队列中,因此终结队列中会保存它的引用,使它的生存期延长。

泛型

泛型类型是参数化的类型。通过定义泛型类型,每个泛型类型内部可以有相同的算法,但数据类型和方法标签却可以按使用者的要求进行个性化实现。

using System.Collections.Generic; // 这个命名空间提供了很多泛型

泛型类

C#有与C++相似的泛型支持,使用尖括号 <> 声明和使用泛型

interface ITwin<T> {
    T First { get; set; }
    T Second { get; set; }
}

public class Twin<T>: ITwin<T> {
    public T First { get; set; }
    public T Second { get; set; }
    public Twin(T first, T second) { // 构造函数不用写成Twin<T>(...)
        First = first;
        Second = second;
    }
    public Twin(T first) {
        First = first;
        Second = default(T); // 使用default对不定类型进行初始化
    }
}

public class Pair<TFirst, TSecond> { // 多个类型参数,System.ValueTuple
    TFirst First { get; set; }
    TSecond Second { get; set; }
    class Rest<U> {
        void Foo(TFirst a1, U a2) {}
    }
}

void F() {
    var TwinString = new Twin<string>("a", "n");
}

规范:命名时在类型参数前加T表示它是一个类型参数

泛型方法

public static T Max<T> (T val, params T[] values)
    where T: IComparable<T>
{
    T max = val;
    foreach (T i in values)
        if (i.CompareTo(max) > 0)
            max = i;
    return max;
}

void F() {
    System.Console.Write(Max(1, 4, 2, 5, 1, 2));
    // 调用泛型方法时会自动推断类型
}

约束

作为约束使用的类型必须是接口、非密封类或类型参数

接口约束
class A<T>
    where T: System.IComparable<T> // 规定T必须实现IComparable接口
{ }
类约束
class A<T>
    where T: B, System.IComparable // 类约束必须第一个出现且只有一个
    // T可以是B类型本身,也可以是B的派生,只要能隐式地转化为B就行
{ }

class B {}
sealed class B {} // 错误:约束不能是sealed
struct/class 约束
class A<T>
    where T: class // 指定引用类型
    // where T: struct // 指定值类型
{ }
构造函数约束
class A<T>
    where T: new()
{
    public A() {
        T n = new T();
        // 要求类型必须有默认构造函数
    }
}
多个约束
class A<T, TFirst, TSecond>
    where T: B, System.IComparable
    where TFirst: System.IComparable<TFirst>
    where TSecond: struct
{ }
约束继承
class Base<T> where T: IComparabel<T> { }
class Derived<U> where U: struct, IComparable<U> {}
// 继承后的约束要重新声明,并且只能比基类强
// 虚函数的继承不用重写约束,但不允许增加约束
约束条件
  • 不能约束一些特殊类型,如 objectSystem.Enum
  • 不能限制类型必须有一个静态方法,如操作符
  • 多个约束必须同时满足
  • 不能约束委托类型、数组类型、枚举类型
  • 构造函数约束只能约束默认构造函数

协变性和逆变性

如果 X 能转换为 Y , 且 I<X> 能转换为 I<Y> 则称 I<T>协变
如果 X 能转换为 Y , 且 I<Y> 能转换为 I<X> 则称 I<T>逆变

LIst<T> 不是协变的

string s = "";
object ob = s;
var strList = new List<string>();
List<object> objList = strList; // Error

如果允许转换,那么 objListstrList 的引用,对 objList 操作会导致 strList 改变,这可能导致错误行为

out 允许协变性
interface IReadOnly<out T> {
    T Val { get; }
} // 只允许读取

数组可以转化为 IEnumerable<T> 只读接口来实现安全的协变

in 允许逆变性
interface ICompareThings<in T> {
    bool ChooseFirst(T first, T second);
} // T不能被读取和作为返回类型
  • 只有泛型接口泛型委托才可以协变和逆变
  • 协变和逆变的两个类型必须是引用类型
  • 类型参数确实是安全的协变或逆变

泛型的优点

  • 减少代码量和工作量
    如果没有泛型,如果想使用以某种数据类型为基础的类型,都必须重新写出一个新的类
  • 提高程序效率
    通过使用泛型,在存储数据时不用object装箱和拆箱,减少内存消耗和时间消耗
  • 促进类型安全
    使用泛型减少object和具体类型的转换,不用进行类型检查,类中的数据类型也更加明确
  • 代码可读性更好
    将具有相同操作的类型统一,抽象程度更高且更易理解

委托和Lambda

使用委托和Lambda

static void F() {
    int[] a = new int[30];
    var rand = new Random();
    for (int i = 0; i < a.Length; ++i) a[i] = rand.Next(0, 30);

    BubbleSort(a, (i, j) => i < j); // Lambda表达式
    foreach (int i in a) Write($"{i} ");
    BubbleSort(a, GreaterThan);
    foreach (int i in a) Write($"{i} ");
}
public static void BubbleSort<T>(T[] arr, Func<T, T, bool> comp) { // 委托
    for (int i = arr.Length; i != 0; --i)
        for (int j = 1; j != i; ++j)
            if (comp(arr[j], arr[j - 1]))
                Swap(ref arr[j], ref arr[j - 1]);
}
public static void Swap<T>(ref T a, ref T b) { T t = a; a = b; b = t; }
public static bool GreaterThan(int left, int right) => left < right;

委托

public delegate TResult Func<in T1, in T2, out TResult>(in T1 arg1, in T2 arg2);
public delegate bool Comparer(int left, int right);

C#自带常用的委托,包括

  • System.Func 有返回值
  • System.Action 无返回值
  • System.Predicate 谓词
委托的内部机制

委托派生自 System.MulticastDelegate ,后者又派生自 System.Delegate ,委托总是直接或间接地派生自 System.Delegate
System.Delegate 包含两个属性,一个是 System.Reflection.MethodInfo 类型 Method ,描述了方法的签名;第二个属性是 object 类型 Target ,包含要调用的方法

Lambda表达式和匿名方法

Lambda表达式匿名方法统称为匿名函数Lambda表达式包括语句Lambda表达式Lambda

语句Lambda
BubbleSort(arr, (int left, int right) => { return left < right; });
BubbleSort(arr, (left, right) => { return left < right; }); // 省略参数类型

() => { return System.Console.ReadLine(); } // 无参
表达式Lambda
BubbleSort(arr, (left, right) => left < right);
Lambda表达式的注意事项
  • Lambda表达式本身没有类型
  • Lambda表达式不能出现在 is as 操作符左侧
  • Lambda表达式只能转换为兼容委托类型
  • Lambda表达式不能用于 var 推断局部类型
  • Lambda表达式内部不能使用跳转语句
  • Lambda表达式中的参数和局部变量作用域在Lambda表达式主体内
  • Lambda表达式中不能对外部局部变量初始化
匿名方法
BubbleSort(arr, delegate(int left, int right) { return left < right; });

delegate { return System.Console.ReadLine() != ""; } // 无参匿名方法,完全省略参数列表

委托转换

Comparer comp = (left, right) => left < right;
Func<int, int, bool> f = comp.Invoke; // Invoke

// void Action<in T>(T arg);
Action<object> actObj = (obj) => obj.ToString();
Action<string> actStr = actObj; // 逆变性,因为有in参数

// TResult Func<in T, out TResult>(T arg)
Func<object, string> f1 = (obj, str) => obj.ToString() + str;
Func<string, object> f2 = f1; // 协变和逆变同时发生

外部变量和循环变量

int count = 0;
BubbleSort(arr, (left, right) => { ++count; return left < right; });
// 被捕捉的变量生存期会延长(作为实例字段实现),因为委托必须能安全的访问外部变量
// 生成的类被称为闭包

string[] items = {"abc", "123", "000"};
var actions = new List<Action>();
foreach (string item in items)
    actions.Add( () => System.Console.Write(item) );
foreach (var action in actions)
    action();
// 在C#5.0前输出000000000,每轮item被认为是同一个变量
// C#5.0输出abc123000,每轮item不是同一个变量
// 在for循环中同C#5.0前

不在匿名函数中捕捉循环变量

事件

定义发布者和订阅者
static void Main()
{
    Action<int> first = (i) => WriteLine(i); // 订阅者1
    Action<int> second = (i) => WriteLine(i * 2); // 订阅者2
    F(12, first + second);
}
static void F(int i, Action<int> actions) // 发布者
{
    actions?.Invoke(i);
}
public class Publisher {
    public Action<float> NoteEvents { get; set; }
    public float Processset {
        set => NoteEvents?.Invoke(value); // ?. 检查空值
    }
}
public class Subscriber1 {
    public void GetNotePos(float note) { if (note > 0) Console.Write($"p: {note}"); }
}
public class Subscriber2 {
    public void GetNoteNeg(float note) { if (note < 0) Console.Write($"n: {note}"); }
}

static void Main() {
    Publisher p = new Publisher();
    Subscriber1 pos = new Subscriber1();
    Subscriber2 neg = new Subscriber2();
    p.NoteEvents += pos.GetNotePos; // +=连接订阅者和发布者
    p.NoteEvents += neg.GetNoteNeg;
    // p.NoteEvents = pos.GetNotePos + neg.GetNoteNeg;
    p.Processset = 45;
    p.NoteEvents -= neg.GetNoteNeg;
    p.Processset = -78;
}
防止异常中断事件
public float Processset {
	set {
        var handlers = NoteEvents;
        if (handlers != null) {
            List<Exception> exceptions = new List<Exception>();
            foreach (Action<float> act in handlers.GetInvocationList()) {
                try {
                    act(value);
                }
                catch (Exception e) {
                    exceptions.Add(e);
                }
            }
            if (exceptions.Count > 0)
                throw new AggregateException("There were exceptions thrown by subscribers");
        }
    }
}
声明事件
public class Publisher {
    public class Args : System.EventArgs {
        public Args(float note) { Note = note; }
        public float Note { get; set; }
    }
    // 使用公共字段,使用event关键字:无法赋值,无法在类外调用
    public event EventHandler<Args> NoteEvents = delegate { }; // 初始化为空白委托,不用检查null
    // public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
    public float Processset {
        set {
            NoteEvents.Invoke(this, new Args(value)); // 传递发布者和参数
        }
    }
}
public class Subscriber1 {
    public void GetNotePos(object sender, Publisher.Args e) { // 参数和EventHandler相同
        if (e.Note > 0) Console.WriteLine($"p: {e.Note}");
    }
}
public class Subscriber2 {
    public void GetNoteNeg(object sender, Publisher.Args e) {
        if (e.Note < 0) Console.WriteLine($"n: {e.Note}");
    }
}
自定义事件
public delegate void NoteHandler(object sender, Args newNote); // 自定义委托
private event NoteHandler _NoteEvents;
public event NoteHandler NoteEvents {
    add {
        _NoteEvents = (NoteHandler)System.Delegate.Combine(value, _NoteEvents);
    }
    remove {
        _NoteEvents = (NoteHandler)System.Delegate.Remove(_NoteEvents, value);
    }
} // 自定义+=和-=方法

集合

集合初始化

List<int> intList = new List<int>() { 1, 2, 3, 4, 5 };
Dictionary<string, int> a = new Dictionary<string, int>()
{
    ["Alice"] = 89,
    ["Bob"] = 15,
    ["John"] = 45
    // {"Alice",89},
    // { "Bob", 15 },
    // { "John", 45 }
};

IEnumerable<T>

IEnumerable<T> 使类变为集合

public void Print<T>(IEnumerable<T> items) {
    for (T item in itmes) // IEnumerable接口实现foreach
        Console.Write(item); // foreach内不修改集合
}
IEnumerator<T>

集合(IEnumerable)返回迭代器(IEnumerator)

List<int> a = new List<int>() { 1, 5, 3, 6, 9 };
// foreach等同于
List<int>.Enumerator e = a.GetEnumerator(); // 每个foreach里都是新的Enumerator
while (e.MoveNext()) {
    int n = e.Current;
    Write(n);
}

标准查询操作符

添加命名空间

using System.Linq;
Where() & Select()
string s = Console.ReadLine();
string[] splited = s.Split(new char[] { ' ', ',', ',' }); // 将字符串分割,可能会有空字符串
IEnumerable<string> numstring = splited.Where(x => x != ""); // 选择非空字符串
IEnumerable<int> nums = numstring.Select(x => int.Parse(x));
int[] a = nums.ToArray(); // 从monad转化为int[]

int[] b = Console.ReadLine().Split(new char[] { ' ', ',', ',' }).Where(s => s != "").Select(x => int.Parse(x)).ToArray();

monad类似于python的生成器

a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9]
even = (i for i in a if i % 2 == 0)
  • 是对操作的附加规则
    比如,调用 ToArray() 时附加了 Select()int.Parse() 规则和 Where()!= "" 规则
  • 不立即执行
    只有真正访问集合中的元素时才会执行,比如调用 ToArray() 时才执行 Select()Where()

由于每次访问monad元素的时候都会进行筛选,所以可以选择立刻将monad集合转化为array(ToArray())

OrderBy() & ThenBy()
string[] s = { "2", "10", "1", "3", "20", "30" };
foreach (var i in s)
    Write(i + " "); // 2 10 1 3 20 30 
WriteLine();
foreach (var i in s.OrderBy(s => s))
    Write(i + " "); // 1 10 2 20 3 30
WriteLine();
foreach (var i in s.OrderBy(s => s.Length).ThenBy(s => s))
    Write(i + " "); // 1 2 3 10 20 30
// 按OrderBy().ThenBy().ThenBy().ThenBy()..的顺序

还有 OrderByDescending() ThenByDescending() 的降序版本

Join() & GroupBy() & GroupJoin()

将两个集合连接

static void Main(string[] args)
{
    Subject[] subjects =
        new Subject[] {
            new Subject { ID = 1, Name = "Math" },
            new Subject { ID = 2, Name = "C#" },
            new Subject { ID = 3, Name = "Physic" },
            new Subject { ID = 4, Name = "English" }
        };
    Student[] students =
        new Student[] {
            new Student { SubjectID = 1, Name = "Alice" },
            new Student { SubjectID = 2, Name = "Bob" },
            new Student { SubjectID = 2, Name = "John" },
            new Student { SubjectID = 4, Name = "Carol" },
            new Student { SubjectID = 4, Name = "Ted" },
        };

    IEnumerable<(int ID, string Name, Subject Subject)> items =
        students.Join(subjects,
                      student => student.SubjectID,
                      subject => subject.ID,
                      (student, subject) => (
                          student.SubjectID, student.Name, subject
                      ));
    foreach (var item in items)
    {
        WriteLine(item.Name);
        WriteLine("\t" + item.Subject);
    }
    /*
    Alice
            1 Math
    Bob
            2 C#
    John
            2 C#
    Carol
            4 English
    Ted
            4 English
    */
    
    IEnumerable<IGrouping<int, Student>> groupStudents =
        students.GroupBy(student => student.SubjectID);
    foreach (IGrouping<int, Student> studentGroup in groupStudents)
    {
        WriteLine(studentGroup.Key);
        foreach (Student student in studentGroup)
            WriteLine("\t" + student);
    }
    /*
    1
            1 Alice
    2
            2 Bob
            2 John
    4
            4 Carol
            4 Ted
    */
    
    IEnumerable<(int ID, string Name, IEnumerable<Student> studentList)> subjectItems =
        subjects.GroupJoin(students,
                           subject =>
                           subject.ID,
                           student => student.SubjectID,
                           (subject, studentList) => (subject.ID, subject.Name, studentList));
    foreach (var item in subjectItems)
    {
        Write(item.Name + ": ");
        foreach (Student student in item.studentList)
            Write("  " + student.Name);
        WriteLine();
    }
    /*
    Math:   Alice
    C#:   Bob  John
    Physic:
    English:   Carol  Ted
    */
}

class Subject
{
	public int ID { get; set; }
	public string Name { get; set; }
	public override string ToString() { return $"{ID} {Name}"; }
}

class Student
{
    public string Name { get; set; }
    public int SubjectID { get; set; }
    public override string ToString() { return $"{SubjectID} {Name}"; }
}
聚合函数
string[] s = { "2", "10", "1", "3", "20", "30" };
WriteLine(s.Count(s => s.Length == 2));
WriteLine(s.Sum(s => s.Length));
WriteLine(s.Average(s => s.Length));
WriteLine(s.Max());
WriteLine(s.Min(s => s.Length));
// 聚合函数Lambda表达式的参数和string[]同名,但不会报错

更多查询操作符和使用方法详见语言集成查询 (LINQ)

查询表达式

int[] b = Console.ReadLine().Split(new char[] { ' ', ',', ',' }).Where(s => s != "").Select(x => int.Parse(x)).ToArray();
// 可以写成更漂亮的形式
int[] a = (from s in Console.ReadLine().Split(new char[] { ' ', ',', ',' })
           where s != ""
           select int.Parse(s)).ToArray();
// 使用了类似SQL的语法
posted @ 2022-10-27 11:52  Violeshnv  阅读(26)  评论(0编辑  收藏  举报