Skip to content
Published at:

【C#】CSharp

NOTE

C# 学习笔记,参考书: C#高级编程

3. 对象和类型

  • 类和结构的区别
  • 类成员
  • 表达式体成员
  • 按值和按引用传递参数
  • 方法重载
  • 构造函数和静态构造函数
  • 只读字段
  • 枚举
  • 部分类
  • 静态类
  • object 类,其他类型都从该类派生而来

3.1 创建及使用类

3.2 类和结构

c#
// class
class PhoneCustomer {
  public const string DayOfSendingBill = "Monday";
  public int CustomerID;
  public string FirstName;
  public string LastName;
}

// sturct
struct PhoneCustomerStruct {
  public const string DayOfSendingBill = "Monday";
  public int CustomerID;
  public string FirstName;
  public string LastName;
}

var myCustomer1 = new PhoneCustomer();
var myCustomer2 = new PhoneCustomerStruct();

3.3 类

  • 字段
  • 常量
  • 方法
  • 属性
  • 构造函数
  • 索引器
  • 运算符重载
  • 事件
  • 析构函数
  • 类型:内部类

3.3.1 字段

3.3.2 只读字段

readonly 关键字; note:不能够着之外的地方被赋值

c#
public class DocumentEditor {
  private static readonly uint s_maxDocuments;
  static DocumentEditor () {
    s_maxDocuments = DoSomethingToFindoutMaxNumber();
  }
}

public class Document {
  private readonly DateTime _creationTime;
  public Document() {
    _creationTime = DateTime.Now;
  }
}

Note: 如果没有赋值,它的值就是其特定数据类型的默认值,或者在声明时给它初始化的值。这适用于只读的静态字段和实例字段。

3.3.3 属性

c#
class PhoneCustomer {
  private string _firstName;
  // 属性访问器
  public string FirstName {
    get {
      return _firstName;
    }
    set {
      _firstName = value;
    }
  }
}

1.具有表达式体的属性访问器

c#
private string _firstName;
public string FirstName {
  get => return _firstName;
  set => _firstName = value;
}

2.自动实现的属性

如果属性的 set 和 get 访问器中没有任何逻辑,就可以使用自动实现的属性 不需要声明私有字段。编译器会自动创建它。使用自动实现的属性,就不能直接访问字段,因为不知道编 译器生成的名称。

c#
// 简写:编译器会去生成随机名的私有属性,也不能直接访问
public int Age { get; set; }

// 简写 + 初始化
// 属性初始化器
public int Age ( get; set; } = 42;

3.属性的访问修饰符

c#
// 权限修饰
public string Name {
  get => return _name;
  private set => _name = value;
}

// 权限修饰 简写
public string Name { get; private set; }

4.只读属性

c#
// 省略set 访问器,就可以创建只读属性
private readonly string _name;
public string Name {
  get => _name;
}

5.自动实现的只读属性

c#
// 属性初始化器 初始化只读属性
public string Id { get; } = Guid.NewGuid().ToString();

public class person {
  // 只读属性也可以显式地在构造函数中初始化
  public Person(string name) = Name = name;
  public String Name { get; }
}

6.表达式体属性 从 C#6 开始,只有 get 访问器的属性可以使用表达式体属性实现。

c#
public class Person {
  public Person(string firstName, string lastName) {
    FirstName = firstName;
    LastName = lastName;
  }
  public string FirstNmae { get; }
  public string LastNmae { get; }
  // 表达式体属性
  public string FullName => $"{FirstName} {LastName}";
}

3.3.4 匿名类型

var 与 new 关键字一起使用时,可以创建匿名 类型。匿名类型只是 一个继承自 Object 且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的 变量。

c#
var caption = new {
  FirstName = "James",
  MiddleName = "T",
  LastName = "Kirk"
};

var doctor = new {
  FirstName = "Leonard",
  MiddleName = string.Empty,
  LastName = "McCoy"
};

// 有相同匿名属性的匿名对象可以赋值
caption = doctor;

如果所设置的值来自于另一个对象,则可以推断匿名类型成员的名称。这样,就可以简化初始化器。

c#
// caption的属性来自person;赋值后caption会具有FirstName、MiddleName 和LastName属性
var caption = new  {
  person.FirstName,
  person.MiddleName,
  person.LastName
}

3.3.5 方法

  1. 方法的声明
c#
// Syntax:
[modifiers] return_type MethodName([parameters]) {
  // Method body
}

// Example:
public bol IsSquare(Rectangle rect) {
  return (rect.Height = = rect.Width);
}
  1. 表达式体方法
c#
public bol IsSquare(Rectangle rect) => rect.Height = = rect.Width;
  1. 调用方法
c#
public class Math {
  public int Value { get; set; }
  public int GetSquare() => Value * Value;
  public static int GetSquareOf(int x) => x * x;
  public static double GetPi() => 3.14159;
}

using System;
namespace MathSample {
  class Program {
    static void Main() {
      // 调用静态方法
      Console.WriteLine($"Pi is {Math.GetPi()}");

      int x = Math.GetSquareOf(5);
      Console.WriteLine($"Square of 5 {x}");

      // 实例对象
      var math = new Math();
      math.Value = 30;
      Console.WriteLine($"Value failed of math variable contains {math.Value}");
      Console.WriteLine($"Square of 30 is {math.GetSquare()}");
    }
  }
}

4.方法的重载

c#
class ResultDisplayer {
  // 重载的参数类型不一样
  public void DisplayResult(string result) {
    // implementation
  }
  public void DisplayResult(int result) {
    // implementation
  }
}

class MyClass {
  // 重载的参数个数不一样
  public int DoSomething(int x) {
    return DoSomething(x, 10);
  }
  public int DoSomething(int x, int y) {
    // implementation
  }
}
  1. 命名的参数
c#
public void MoveAndResize(int x, int y, int width, int height) {
  // implementatioin
}

r.MoveAndResize(30, 40, 20, 40);
// 显示写上参数名:代码可读性更高
r.MoveAndResize(x: 30, y: 40, width: 20, height: 40);

6.可选参数

c#
public void TestMethod(int notOptionalNumber, int optionalNumber = 42) {
  Console.WriteLine(optionalNumber + notOptionalNumber);
}

TestMethod(11);
TestMethod(11, 22);

// 多个可选参数
public void TestMethod(int n, int opt1 = 11, int opt2 = 22, int opt3 = 33) {
  Console.WriteLine(n + opt1 + opt2 + opt3);
}

TestMethod(1);
TestMethod(1, 2, 3);
TestMethod(1, opt3: 4); // 指定后面的某个参数
  1. 个数可变的参数

使用可选参数,可以定义数量可变的参数。params 关键字

c#
public void AnyNumberOfArguments(params int[] data) {
  foreach(var x in data) {
    Console.WriteLine(x);
  }
}

AnyNumberOfArguments(1);
AnyNumberOfArguments(1, 3, 5, 7, 11);


// object
public void AnyNumberOfArguments(params object[] data) {
  // implementation
}

AnyNumberOfArguments("text", 42);

3.3.6 构造函数

c#
public class Myclass {
  private string _foo;
  public MyClass() { }
 `public MyClass(int number) { }
 `public MyClass(string foo) {
    _foo = foo;
  }
}

//
public class Singleton {
  private static Singleton s_instance;
  private int _state;
  private Singleton(int state) {
    _state = state;
  }

  public static Singleton Instance {
    get => s_instance ? ( s_instance = new Singleton(42);
  }
}

1.表达式体和构造函数

c#
public class Singleton {
  private static Singleton s_instance;
  private int _state;
  private Singleton(int state) => _state = state;

  public static Singleton Instance =>
    s_instance ?? s_instance = new Singleton(42);
}

2.从构造函数中调用其他构造函

c#
class Car {
  private string _description;
  private uint _nWheels;

  public Car(string description, uint nWheels) {
    _description = description;
    _nWheels = nWheels;
  }

  public Car(string description) {
    _description = description;
    _nWheels = 4;
  }

  // 或者 使用构造函数初始化器:调用其它构造
  public Car(string description): this(description, 4) {
  }
}

3.静态构造

C#的 一个特征是也可以给类编写无参数的静态构造函数。这种构造函数只执行一次,而前面的构造函数是 实例构造函数,只要创建类的对象,就会执行它。

c#
class MyClass {
  static MyClass() {
    // initialization code
  }
  // rest of class definition
}

示例:

c#
public enum Color {
  White,
  Red,
  Green,
  Blue,
  Black,
}

public static class UserPreferences {
  public static Color BackColor { get; }
  static UserPreferences() {
    DateTime now = DateTime.Now;
    if (now.DayOfWeek == DayOfWeek.Sunday) {
      BackColor = Color.Green;
    } else {
      BackColor = Color.Red;
    }
  }
}

class Program {
  static void Main() {
    Console.WriteLine($"User-preferences: BackColor is: {UserPreferences.BackColor}");
  }
}

3.4 结构

结构是值类型,不是引用类型。它们存储在栈中或存储为内联,其生存期的限制与简单的数据类型一样。

  • 结构不支持继承
  • 对于结构,构造函数的工作方式有一些区别。如果没有提供默认的构造函数,编译器会自动提供一个,把成员初始化为其默认值
  • 使用结构,可以指定字段如何在内存中布局
c#
public struct Dimensions {
  public double Length{ get; }
  public double Width{ get; }
  public Dimensions(double length, double width) {
    Length = length;
    Width = width;
  }
  public double Diagonal => Math.Sqrt(Length * Length + Width * Width);
}

3.5 按值和按引用传递参数

值传递的局限性

3.5.1 ref 参数

3.5.2 out 参数

3.5.3 in 参数

3.6 可空类型

引用类型(类)的变量可以为空,而值类型(结构)的变量不能 C#有一个解决方案:可空类型。可空类型是可以为空的值类型

c#
int x1 = 1;
int? x2 = null;

// 赋值
int? x3 = x1;
int x4 = (int)x3; // 需要强转

// 可空类型的HasValue和Value属性
int x5 = x3.HasValue ? x3.Value : -1;

// 合并操作符`??`;是上面的简写
int x6 = x3 ?? -1;

3.7 枚举类型

枚举是一个值类型,包含一组命名的常量

c#
public enum color {
  Red,
  Green,
  Blue
}

private static vodi colorsamples() {
  Color c = Color.Red;
  Console.WriteLine(c);
}

默认情况下,enum 的类型是 int。这个基本类型可以改为其他整数类型(byte、short、int、带符号的 long 和无符号变量)。命名常量的值从。开始递增

c#
public enum Color : short {
  Red = 1,
  Green = 2,
  Blue = 3
}

// 枚举和其它类型强制转换
Color c2 = (Color)2;
short number = (short)c2;

还可以使用 enum 类型把多个选项分配给一个变量,而不仅仅是一个枚举常量。为此,分配给常量的值必须是不同的位,Flags 属性需要用枚举设置。 枚举类型 DaysOfWeek 为每天定义了不同的值。要设置不同的位,可以使用用 0x 前缀指定的十六进制值轻松地完成,

c#
[Flags]
public enum DaysOfWeek {
  Monday = 0x1,
  Tuesday = 0x2,
  Wednesday = 0x4,
  Thursday = 0x8,
  Friday = 0x10,
  Saturday = 0x20,
  Sunday = 0x40,
}

DaysOfWeek mondayAndWednesday = DaysOfWeek.Monday | DaysOfWeek.Wednesday;
Console.WriteLine(mondayAndWednesday);
// 输出(字符串):Monday, Tuesday

设置不同的位,也可以结合单个位来包括多个值

c#
[Flags]
public enum DaysOfWeek {
  Monday = 0x1,
  Tuesday = 0x2,
  Wednesday = 0x4,
  Thursday = 0x8,
  Friday = 0x10,
  Saturday = 0x20,
  Sunday = 0x40,
  Weekend = Saturday | Sunday,
  Workday = 0x1f,
  AllWeek = WorkDay | Weekend
}

DaysOfWeek Weekend = DaysOfWeek.Saturday | DaysOfWeek.Sunday;
Console.WriteLine(mondayAndWednesday);

类 Enum 有时非常有助于动态获得枚举类型的信息:

  • Enum.TryParse: 解析字符串,获得相应的枚举常数,获得枚举类型的所有名称和值
  • Enum.GetNames: 返回 一个包含所有枚举名的字符串数组
  • Enum.GetValues: 返回枚举值的一个数组
c#
// Enum.TryParse
Color red;
if (Enum.TryParse<Color>("Red", out red) {
  Console.WriteLine(S"successfully parsed {red}");
}

// Enum.GetNames
foreach (var day in Enum.GetNames(typeof(Color))) {
  Console.WriteLine(day);
}

// Enum.GetValues:
foreach (var day in Enum.GetValues(typeof(Color))) {
  Console.WriteLine(day);
}

3.8 部分类

partial 关键字允许把类、结构、方法或接又放在多个文件中。假定要给类添加一些从工具中自动生成的内容(代码)

3.9 扩展方法

扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中

c#
public static class StringExtension {
  public static int GetWordCount(this string s) => s.Split().Length;
}

string fox = "the quick brown fox jumped over the lazy dogs down 9876543210 times";
int wordCount = fox.GetWordCount();
Console.WriteLine($"{wordCount} words");

// 编译器会把 `int wordCount = fox.GetWordCount();` 改为下面代码:
int wordCount = StringExtension.GetWordCount(fox);

3.10 Object 类

所有的.NET 类最终都派生自 System.Object。如果在定义类时没有指定基类,编译器就会自动假定这个类派生自 Object。 (对于结构体 sturct,这个派生是间接的:结构总是派生自 System.ValueType, System.ValueType 又派生自 System.Object

常用方法:

  • ToString(): 获取对象的字符串表示
  • GetHashCode(): 如果对象放在名为映射 Map 的数据结构中,就可以使用这个方法
  • Equals()和ReferenceEquals(): 用于比较类型是否相等
  • Finalize(): 接近 C++风格的析构函数,在引用对象作为垃圾被收集 以清理资源时调用它。
  • GetType(): 这个方法返回从System.Type派生的类的一个实例,因此可以提供对象成员所属类的更多信息,包括基本类型、方法、属性等
  • MemberwiseClone(): 该方法不是虚方法,所以不能重写它的实现代码;用的少

运算符和强制类型转换

6.2 运算符

  • 类型信息运算符: sizeofistypeofas
  • 溢出异常控制运算符: checkunchecked
  • 空合并运算符: ??
  • 标识符的名称运算符: nameof

6.2.1 运算符的简化操作

8. 委托、lambda 表达式和事件

8.1 引用方法

委托是寻址方法的.NET 版本。在 C++中,函数指针只不过是 一个指向内存位置的指针,它不是类型安全的。我们无法判断这个指针实际指向什么,参数和返回类型等项就更无从知晓了

而.NET 委托完全不同;委托是类型安全的类,它定义了返回类型和参数的类型。委托类不仅包含对方法的引用,也可以包含对多个方法的引用。 lambda 表达式与委托直接相关。当参数是委托类型时,就可以使用 lambda 表达式实现委托引用的方法。

8.2 委托

8.2.1 声明委托

c#
delegate void IntMethodInvoker (int x);
delegate double IwoLongsop (long first , long second);
delegate string GetAString () ;

// class 内(加上权限修饰)
public delegate string GetAString();

8.2.2 使用委托

hello world 示例:

c#
private delegate string GetAString();

public static void Main() {
  int x = 40;
  GetAString firstStringMethod = new GetAString(x.ToString);
  Console.WriteLine($"String is {firstStringMethod()}");
}
c#
// 初始化
GetAString firstStringMethod = new GetAString(x.ToString);
GetAString firstStringMethod = x.ToString;

// 调用
firstStringMethod();
firstStringMethod.Invoke(); // C#编译器会用firstStingMethod.Invoke()代替firstStringMethod()

注意: 实际上,“定义一个委托” 是指“定义一个新类” 。委托实现为派生自基类 System. MulticastDelegat e 的类, System.MulticastDelegate 又派生自基类 System.Delegate。

8.2.4 Action<T>Func<T> 委托

委托和泛型结合

  • Action<T>委托表示引用一个 void 返回类型的方法
    • Action<in T>调用带一个参数的方法
    • Action<in TI, in T2>调用带两个参数的方法,
    • Action<in TI, in T2.in T3, in T4, in T5, in T6, in T7, in T8>调用带 8 个参数的方法
  • Func<T>允许调用带返回类型的方法
    • Func<out TResult> 委托类型可以调用带 返回类型且无参数的方法,
    • Func<in T, out TResult> 调用带一个参数的方法,
    • Func<in T1, in T2, in T3, in T4, out TResult> 调用带 4 个参数的方法
c#
delegate double Doubleop (double x);
Func<double, double>[] operations = {
  Mathoperations MultiplyByIwo, Mathoperations Square
};

static void ProcessAndDisplayNumber (Func<double, double> action, double value) {
  double result = action(value) ;
  Console.WriteLine($"value is {value}, result of operation is {result}");
}

8.2.6 多播委托

委托可以存放多个函数;多播表示会调用多个函数指针

c#
class Mathoperations {
  public static void MultiplyByIwo (double value) {
    double result = value * 2;
    Console.WriteLine($"Multiplying by :2 {value} gives {result}");
  }

  public static void Square (double value) {
    double result = value * value;
    Console.Writeline($"squaring: {value} gives {result}");
  }
}

static void Main() {
  Action‹double>operations=Mathoperations MultiplyByIwo;
  operations += MathOperations.Square;

  ProcessAndDisplayNumber(operations, 2.0); // 会调用上面两个函数
  ProcessAndDisplayNumber(operations, 7.94);
  ProcessAndDisplayNumber(operations, 1.414) ;
  Console.WriteLine();
}

Note: 多播委托的调用顺序无法保证

8.2.7 匿名方法

c#
class Program {
  static void Main() {
    string mid = ", middle part,";
    Func<string, string> anonDel = delegate (string param) {
      param =+ mid;
      param = " and this was added to the string.";
      return param;
    };
    Console.WriteLine(anonDel("Start of string"));
  }
}

8.3 lambda 表达式

lambda 运算符“=>” 的左边列出了需要的参数,而其右边定义了赋予 lambda 变量的方法的实现代码;和委托结合

c#
class Program {
  static void Main() {
    string mid = ", middle part,";
    Func<string, string> anonDel = param => {
      param =+ mid;
      param = " and this was added to the string.";
      return param;
    };
    Console.WriteLine(anonDel("Start of string"));
  }
}

8.4 事件

事件基于委托,为委托提供了一种发布/订阅机制

8.4.1 事件发布程序

c#
using System;
namespace Wrox. ProCSharp. Delegates {
  public class CarInfoEventArgs: EventArgs {
    public CarInfoEventArgs(string car) => Car = car;
    public string Car( get; )
  }

  public class CarDealer {
    public event EventHandler<CarInfoEventArgs> NewCarInfo;
    public void NewCar(string car) {
      Console.WriteLine($"CarDealer, new car {car}");
      NewCarInfo?.invoke(this, new CarInfoEventArgs(car));
    }
  }
}

event 解释:

c#
public event EventHandler<CarInfoEventArgs> NewCarInfo;

// 委托EventHandler<TEventArgs >的定义如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs: EventArgs

8.4.2 事件侦听器

c#
public class Consumer {
  private string _name;
  pubLci Consumer(string name) => _name = name;
  public void NewcarIsHere(object sender, CarInfoEventArgs e) {
    Console.WriteLine($" {name}: car {e.Car} si new");
  }
}

class Program {
  static void Main() {
    var dealer = new CarDealer();

    // add
    var valtteri = new Consumer("Valtteri");
    dealer.NewCarInfo += valtteri.NewcarIsHere;
    dealer.NewCar("Williams");

    // add
    var max = new Consumer("Max");
    dealer.NewCarInfo += max.NewcarIsHere;
    dealer.NewCar("Mercedes");

    // delete
    dealer.NewCarInfo =- valtteri.NewCarIsHere;
    dealer.NewCar ("Ferrari") ;
  }
}

9 字符串和正则表达式

  • 创建字符串

  • 格式化表达式

  • 使用正则表达式

  • 使用 Span<T>和字符串

  • 构建字符串:String 类平时用;System.Text.StringBuilder类构建大的字符串

  • 格式化表达式:接又 IFormatProviderIFoumattable 来处理

  • 正则表达式:

  • Span:.NETCore 提供了泛型 Span 结构,它允许快速访问内存。Span<T>允许访问字符串的切片,而不需要复制字符串

9.1 System.String 类

c#
// create
string message1 = "hello";
message1 += ", world";

string message2 = message1 + "!";

// get char
string message = "Hello";
char char4 = message[4]; // returns 'o'. Note the string is zero-indexed

常用方法:

  • Compare:
  • CompareOrdinal:
  • Concat:
  • CopyTo:
  • Format:
  • IndexOf:
  • IndexOfArray:
  • Insert:
  • Join:
  • LastIndexOf:
  • LastIndexOfArray:
  • PadLeft:
  • PadRight:
  • Replace:
  • Split:
  • Substring:
  • ToLower:
  • ToUpper:
  • Trim:

9.1.2 StringBuilder 成员

StringBuilder 类不像 String 类那样能够支持非常多的方法。在 StingBuilder 类上可以进行的处理仅限于替换和追加或删除字符串中的文本。但是,它的工作方式非常高效。

StringBuilder 类有两个主要的属性:

  • Length:指定包含字符串的实际长度。
  • Capacity:指定字符串在分配的内存中的最大长度
c#
// 指定内容
var sb = new StringBuilder("Hello") ;
// 指定容量
var sb = new StringBuilder(20) ;

常用方法:

  • Append()
  • AppendFormat()
  • Insert()
  • Remove()
  • Replace()
  • ToString()

9.2 字符串格式

c#
// example1
string s1 = "World";
string s2 = §"Hello, {s1}";

// $是语法糖,对于带$前缀的字符串,编译器创建Sting.Format 方法的调用
string s1 = "World";
string s2 = String.Format("Hello, {0}", s1);

// example2
string 2s = S"Hello, (s1.ToUpper ()}";
// 这段代码可解读为如下类似的语句:
string s2 = String. Format ("Hello, (0)", s1.ToUpper ());

转义花括号

c#
string s ="Hello";
Console.WriteLine ($"((s)} displays the value of s: (s)");

9.4 字符串和 Span

今天的编程代码通常处理需要操作的长字符串。例如,WebAPI 以 JSON 或 XML 格式返回一个长字符串。将如此大的字符串分割成许多更小的字符串,意味着创建了许多对象,而垃圾收集器不再需要这些字符串时,需要做很多事情来释放这些字符串所占的内存。 .NETCore 有一个新的方法: Span<T>类型。该类型引用数组的一个切片,而不需要复制它的内容。同样,Span<T>可以用来引用一个字符串的片段,而不需要复制原始内容

c#
int ix = text.IndexOf ("Visual");
ReadOnlySpan<char> spanToText = text.AsSpan() ;
ReadOnlySpan<char> slice = spanToText.Slice(ix, 13);
string newString = new string(slice.ToArray());

10 集合

10.2 集合接又和类型

大多数集合类都可在 System.CollectionsSystem.Collections.Generic 名称空间中找到。

  • 泛型集合类位于 SystemCollections.Generic 名称空间中;
  • 专用于特定类型的集合类位于 System.Collections.Specialized 名称空间中。
  • 线程安全的集合类位于 System.Collections.Concurent 名称空间中。
  • 不可变的集合类在 SystemCollectionsImmutable 名称空间中。

集合和列表实现的接口列表:

  • IEnumerable<T>
  • ICollection<T>
  • IList<T>
  • ISet<T>
  • IDictionary<TKey, TValue>
  • ILookup<TKey, TValue>
  • IComparer<T>
  • IEqualityCompare<T>

15. 异步编程

• 异步编程的重要性 • 异步模式 • 异步编程的基础 • 异步方法的错误处理 • Windows 应用程序的异步编程

15.1 异步编程的重要性

异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并且不会阻塞调用线程。 .NETFramework 4.5 将任务并行库(Task Parauel Library, TPL)添加到.NET 中,以使并行编程更容易。C#5.0 增加了两个关键字来简化异步编程 : asyncawait

3 种不同模式的异步编程:

  • 异步模式
  • 基于事件的异步模式
  • 基于任务的异步模式(Task-based AsynchronousPatter, TAP), 用 async 和 await 来实现

15.2 异步编程的.NET 历史

  • .NETFramework 1.0 开始提供了异步特性。.NETFramework 的许多类都实现了一个或者多个异步模式
  • .NETFramework 2.0 推出了基于事件的异步模式
  • .NETFramewouk 4.5 中,推出基于任务的异步模式(TAP)。基于 Task 类型,并通过 asyncawait 关键字使用编译器功能

15.2.1 同步调用

会卡信执行流程

c#
private const string url = "http://www.cninnovation.com";
private static void SynchronizedAPI() {
  Console.WriteLine(nameof(SynchronizedAPI));
  using (var client = new WebClient()) {
    string content = client.DownloadString(url);
    Console.WriteLine(content.Substring(0, 100));
  }
  Console.WriteLine();
}

15.2.2 异步模式

异步模式定义了 BeginXXX 方法和 EndXXX 方法。例如,如果有 一个同步方法 DownloadSting,其异步 版本就是 BeginDowloadString 和 EndDownloadString 方法。 BeginXXX 方法接受其同步方法的所有输入参数,EndXXX 方法使用同步方法的所有输出参数,并按照同步方法的返回类型来返回结果。使用异步模式时, BeginXXX 方法还定义了一个 AsyncCallback 参数,用于接受在异步方法执行完成后调用的委托。BeginXXX 方法返回 IAsyncResult,用于验证调用是否已经完成,并且一直等到方法的执行结束。

c#
private static void AsynchronousPattern() {
  Console.WriteLine(nameof(AsynchronousPattern));
  WebRequest request = WebRequest.Create(url);
  IAsyncResult result = request.BeginGetResponse(ReadResponse, null);

  void ReadResponse(IASyncResult ar) {
    using (ReadResponse response = request.EndGetResponse(ar)) {
      Stream stream = response.GetResponseStream();
      var reader = new StreamReader(stream);
      string content = reader.ReadToEnd();
      Console.WriteLine(content.Substring(0, 100));
      Console.WriteLine();
    }
  }
}

15.2.3 基于事件的异步模式

.NETFramework 2.0 推出了基于事件的异步模式 线程问题,异步调用发起者、异步调用完成处理者,可以不在同的一线程;比如,GUI 编程中,异步调用通常是 UI 来发起,调用完成之后会把数据回传到 UI 去显示。

c#
private static void EventBasedAsyncPattern () {
  Console.Writeline(nameof(EventBasedAsyncPattern));
  using (var client = new WebClient()) {
    // DownloadStringCompletedEventHandler DownloadStringCompleted;
    // DownloadStringCompletedEventArgs e;
    client.DownloadStringCompleted =+ (sender, e) => {
      // 事件处理程序将通过保存同步上下文的线程来调用,这个就是UI线程
      Console.WriteLine (e.Result.Substring(0,100));
    };
    client.DownloadStringAsync(new Uri(url));
    Console.Writeline();
  }
}

15.2.4 基于任务的异步模式

NET Framework 4.5 提供了基于任务的异步模式(TAP),该模式定义了一个带有“Async”后缀的方法,并返回一个 Task 类型

c#
private static async Task TaskBasedAsyncPatternAsync() {
  Console.WriteLine(nameof(TaskBasedAsyncPatternAsync));
  using (var client = new WebClient()) {
    // client.DownloadStringTaskAsync 返回Task<string>
    // await关键字会解除线程(这里是UI线程)的阻塞,完成其他任务。当DownloadStringTaskAsync方法完成其后台处理后,UI线程就可以继续,从后台任务中获得结果,赋值给字符串变量
    string content = await client.DownloadStringTaskAsync(url);
    Console.WriteLine(content.Substring(0, 100));
    Console.WriteLine();
  }
}
  • async 关键字创建了一个状态机

代码顺序也和惯用的同步编程一样。

15.2.5 异步 Main()方法

需要 C# 7.1

c#
static async Task Main() {
  SynchronizedAPI();
  AsynchronousPattern();
  EventBasedAsyncPattern();
  await TaskBasedAsyncPatternAsync();
  Console.ReadLine();
}

15.3 异步编程的基础

async 和 await 关键字只是编译器功能。编译器会用 Task 类创建代码

  • 编译器用 async 和 await 关键字能做什么
  • 如何采用简单的方式创建异步方法
  • 如何并行调用多个异步方法
  • 以及如何修改已经实现异步模式的类,以使用新的关键字

任务 id 和线程 id

c#
public static void TraceThreadAndTask(string info) {
  string taskInfo = Task.CurrentId == null ? "no task" : "task " + Task.CurrentId;

  Console.WriteLine($"{info} in thread {Thread.CurrentThread.ManagedThreadId()}" + $“and {taskInfo}”);
}

15.3.1 创建任务

c#
static string Greeting(string name) {
  TraceThreadAndTask($"running {nameof(Greeting)}");
  Task.Delay(3000).Wait();
  return $"Hello, {name}";
}

static Task<string> GreetingAsync(string name) => Task.Run<string>(() => {
  TraceThreadAndTask($"running {nameof(Greeting)}");
  return Greeting(name);
});

15.3.2 调用异步方法

c#
private async static void CallerWithAsync() {
  TraceThreadAndTask($"started {nameof(CallerWithAsync)}");
  string result = await GreetingAsync("Setphanie");
  Console.WriteLine(result);
  TraceThreadAndTask($"ended {nameof(CallerWithAsync)}");
}

15.3.3 使用 Awaiter

可以对任何提供 GetAwaiter 方法并返回 awaiter 的对象使用 async 关键字。awaiter 用 OnCompleted 方法实现 INotifyCompletion 接又。此方法在任务完成时调用。

c#
private static void CallerWithAwaiter() {
  TraceThreadAndTask($"starting {nameof(CallerWithAwaiter)}");
  TaskAwiter<string> awaiter = GreetingAsync("Matthias").GetAwaiter();
  awaiter.OnCompleted(OnCompleteAwaiter);

  void OnCompleteAwaiter() {
    Console.WriteLine(awaiter.GetResult());
    TraceThreadAndTask($"ended {nameof(CallerWithAwaiter)}");
  }
}

15.3.4 延续任务

Task 类的 ContinueWith 方法定义了任务完成后就调用的代码。指派给 ContinueWith 方法的委托接收将已完成的任务作为参数传入,使用 Result 属性可以访问任务返回的结果:

c#
private static void CallerWithContinuationTask() {
  TraceThreadAndTask($"started {nameof(CallerWithAwaiter)}");
  var t1 = GreetingAsync("Stephanie");
  t1.ContinueWith(t => {
    string result = t.Result;
    Console.WriteLine(result);
    TraceThreadAndTask($"ended {nameof(CallerWithAwaiter)}");
  })
}

15.3.5 同步上下文

异步带来了不确定性(执行的线程不确定);有些(比如 GUI)应用只能在 UI 线程去更新 UI,所以有一种方式可以去指定在特定的线程上执行:

  • 如果使用 async 和 await 关键字,当 await 完成之后,不需要进行任何特别处理,
  • WPF 应用程序设置了 DispatcherSynchronizationContext 属性
  • Windows Forms 应用程序设置了 WindowsFormsSynchronization Context 属性
  • Windows 应用程序使用 WinRTSynchronizationContext
  • etc

15.3.6 使用多个异步方法

  1. 顺序调用
c#
private async static void MultipleAsyncMethods() {
  string s1 = await GreetingAsync("Stephanie");
  string s2 = await GreetingAsync("Matthias");
  Console.WriteLine("$"Finished both methods. {Environment.NewLine} " + $"Result 1: {s1}{Evnironment.NewLine} Result 2: {s2}")
}
  1. 组合器

Task 类定义了 When All 和 When Any 组合器。从 WhenAII 方法返回的 Task ,是在所有传入方法的任务都完 成了才会返回 Task。从 WhenAny 方法返回的 Task,是在其中一个传入方法的任务完成了就会返回 Task。

c#
private async static void MultipleAsyncMethodsWithCombinators() {
  Task<string> t1 = await GreetingAsync("Stephanie");
  Task<string> t2 = await GreetingAsync("Matthias");
  await Task.WhenAll(t1, t2);
  Console.WriteLine("$"Finished both methods. {Environment.NewLine} " + $"Result 1: {t1.Result}{Evnironment.NewLine} Result 2: {t2.Result}");
}

Task 类型的 WhenAII 方法定义了几个重载版本。如果所有的任务返回相同的类型,那么该类型的数组可用 于 await 返回的结果

c#
private async static void MultipleAsyncMethodsWithCombinators2() {
  Task<string> t1 = await GreetingAsync("Stephanie");
  Task<string> t2 = await GreetingAsync("Matthias");
  string[] result = await Task.WhenAll(t1, t2);
  Console.WriteLine("$"Finished both methods. {Environment.NewLine} " + $"Result 1: {result[0]}{Evnironment.NewLine} Result 2: {result[1]}");
}

15.3.7 使用 ValueTasks

C#7 带有更灵活的 await 关键字;它现在可以等待任何提供 GetAwaiter 方法的对象。一种可用于等待的新类型是 ValueTask。

15.3.8 转换异步模式

15.4 错误处理

c#
static async Task ThrowAfter(int ms, string message) {
  await Task.Delay(ms);
  throw new Exception(message);
}

如果调用异步方法,并且没有等待,就会捕获不到异常

c#
private void DontHanle() {
  try {
    // NOte: 调用异步没有await会捕获不到异常
    Throwafter(2000, "first");
  } catch (Exception ex) {
    Console.WriteLine(ex.message);
  }
}

15.4.1 异步方法的异常处理

c#
private void HanleOneError() {
  try {
    await Throwafter(2000, "first");
  } catch (Exception ex) {
    Console.WriteLine(ex.message);
  }
}

15.4.2 多个异步方法的异常处理

如果调用两个异步方法,每个都会抛出异常,该如何处理呢?

c#
private static async void StartTwoTasks() {
  try {
    // throw excption
    await Throwafter(2000, "first");
    await Throwafter(1000, "second"); // Note: 这个函数不会被调用
  } catch (Exception ex) {
    Console.WriteLine(ex.message);
  }
}

并行调用这两个 ThrowAfter 方法。

  • Task.WhenAll 会等到所有历 Task 运行结束
  • 但异常捕获只会捕获到一个异常
c#
private static async void StartTwoParallel() {
  try {
    // throw excption
    Task t1 = Throwafter(2000, "first");
    Task t2 = Throwafter(1000, "second");
    awiat Task.WhenAll(t1, t2); // 会等到两个Task都运行结束,
  } catch (Exception ex) {
    // 这里只会捕获到一个异常
    Console.WriteLine(ex.message);
  }
}

15.4.3 使用 AggregateException 信息

c#
private static async void ShowAggregatedException() {
  Task taskResult = null;
  try {
    // throw excption
    Task t1 = Throwafter(2000, "first");
    Task t2 = Throwafter(1000, "second");
    awiat Task.WhenAll(t1, t2); // 会等到两个Task都运行结束,
  } catch (Exception ex) {
    // 这里只会捕获到一个异常
    Console.WriteLine(ex.message);
    foreach(var tmpEx in taskResult.Exception.InnerExceptions) {
      Console.WriteLine(tmpEx.message);
    }
  }
}

15.5 异步与 Windows 应用程序

15.5.1 配置 await

15.5.2 切换到 UI 线程

15.5.3 使用 IAsyncOperation

15.5.4 避免阻塞情况

19. 库、程序集、包和 NuGet

• 库、程序集和包之间的差异 • 创建库 • 使用,NET 标准 • 使用共享项目 • 创建 NuGet 包

19.1 库的地狱

库可以在多个应用程序中重用代码。

存在一个库多个版本、及兼容性的问题,.NET 用程序集去解决。程序集是可以共享的库,包含

  • 有正常的 DLL
  • 还包含可扩展的元数据
  • 以及关于库和版本号的信息
  • 并且可以在全局程序集缓存中并排安装多个版本。

NuGet 包在库中添加了另一个抽象层。NuGet 包可以包含一个或多个程序集的多个版本,以及其他内容,例如程序集重定向的自动配置。

19.2 程序集

程序集是包含额外元数据的库或可执行文件。 可以使用 ildasm.exe (IL 反汇编程序)命令行实用程序读取程序集信息

  • MANIFEST 元数据信息

19.3 创建库

在 Visual Studio2017 中,有许多创建库的选项:

• Class Library (.NET Core) • Class Library (.NET Standard) • Class Library (.NET Framework) • WPF Custom Control Library (.NET Framework) • WPF User Control Library (.NET Framework) • Windows Forms Control Library (.NET Framework) • Class Library (Universal Windows) • Class Library (Legacy Portable) • Shared Project

19.3.1 .NET 标准

19.3.2 创建.NET 标准库

项目 TargetFramework 属性值

19.3.3 解决方案文件

解决方案文件:组合多个项目时使用,通常是一个程序加上一个或多个库

shell
# 创建解决方案
$ dotnet new sin
# 添加项目
$ dotnet sln add Simplelib/Simplelib.csproj

19.3.4 引用项目

shell
$ dotnet add reference ..\Simplelib\Simplelib.csproj

ItemGroup/ProjectReference

19.3.5 引用 NuGet 包

shell
$ dotnet add package Microsoft.Composition

ItemGroup/ProjectReference

19.3.6 NuGet 的来源

NuGet.Config 配置文件,设置源: packageSources/add(key/value);可配置远程或本地

19.3.7 使用.NETFramework 库

分析兼容性:.NET 可移植性分析器(NETPortability Analyzer) https://github.com/microsof/dotnet-apiport

19.4 使用共享项目

19.5 创建 NuGet 包

19.5.1 NuGet 包和命令行

19.5.2 支持多个平台

第 21 章 任务和并行编程

  • 多线程概述
  • 使用 Parallel
  • 使用任务
  • 使用取消架构
  • 使用数据流库
  • 使用计时器
  • 理解线程问题
  • 使用 lock 关键字 • 用监视器同步
  • 用互斥同步
  • 使用 Semaphore 和 SemaphoresSlim
  • 使用 ManualResetEvent、AutoResetEvent 和 CountdownEvent
  • 处理障碍
  • 用 Read erWriterLockSlim 管理读取器和写入器

21.1 概述

.NET 提供了线程的一个抽象机制:任务。除了使用任务之外,还可以使用 Parallel 类实现并行活动。需要区分数据并行(在不同的任务之间同时处理一些数据)和任务并行性(同时执行不同的功能)。

21.2 Parallel 类

Parallel.For()Parallel.ForEach()方法在每次迭代中调用相同的代码,而Parallel.Invoke()方法允许同时调用不同的方法。Parallel.Invoke()用于任务并行性,而Parallel.ForEach()用于数据并行性。

21.2.1 使用 Parallel.For()方法循环

使用 Parallel.For() 方法,可以并行运行迭代。迭代的顺序没有定义

log 输出代码

c#
public static void Log(string prefix) =>
  Console.WriteLine($"{prefix}, task: {Task.CurrentId}, " + $"thread: {Thread.CurrentThread.ManagedThreadId}");

Parallel.ForTask.Delay(10).Wait()示例

c#
public static void ParallelFor() {
  ParallelLoopResult result = Parallel.For(0, 10, i => {
    Log($"S {i}");
    Task.Delay(10).Wait();
    Log($"E {i}");
  });
  // 所有执行完都会输出
  Console.WriteLine($"Is completed: {result.IsCompleted}");
}

Parallel.Forasync/await示例

c#
public static void ParallelForWithAsync() {
  ParallelLoopResult result = Parallel.For(0, 10, async i => {
    Log($"S {i}");
    await Task.Delay(10);
    Log($"E {i}");
  });
  // Parallel 类只等待它创建的任务,而不等待其他后台活动(async);不等待await
  Console.WriteLine($"Is completed: {result.IsCompleted}");
}

21.2.2 提前中断 Parallel.For

For()方法的一个重载版本接受Actionc<int, ParallelLoopState>类型的第 3 个参数。使用这些参数定义一个方法,就可以调用 ParallelLoopState 的Break()Stop()方法,以影响循环的结果。

c#
public static void StopParallelForEarly() {
  ParallelLoopResult result = Parallel.For(10, 40, (int i, ParallelLoopState pls) => {
    Log($"S {i}");
    if (i > 12) {
      pls.Break();
      Log($"break now ... {i}");
    }
    Task.Delay(10).Wait();
    Log($"E {i}");
  });
  Console.WriteLine($"Is completed: {result.IsCompleted}");
  Console.WriteLine"lowest break iteration: (result.LowestbreakIteration}") ;
}

21.2.3 Parallel.For()方法的初始化

Parallel.For()方法使用几个线程来执行循环。如果需要对每个线程进行初始化,就可以使用 Paraillel.For<TLocal>()方法。除了 from 和 to 对应的值之外,For()方法的泛型版本还接受 3 个委托参数。

  • 第一个参数的类型是 Func<TLocal>。因为这里的例子对于 TLocal 使用字符串,所以该方法需要定义为 Func<string>, 即返回 string 的方法。这个方法仅对用于执行迭代的每个线程调用一次。
  • 第二个委托参数为循环体定义了委托。在示例中,该参数的类型是 Func<int, ParallelLoopState, string, string>。其中第一个参数是循环迭代,第二个参数 ParallelLoopState 允许停止循环,如前所述。循环体方法通过 第 3 个参数接收从 init 方法返回的值,循环体方法还需要返回一个值,其类型是用泛型 For 参数定义的。
  • 最后一个参数指定一个委托 Action<TLocal>; 在该示例中,接收一个字符串, 这个方法仅对于每个线程调用一次,这是一个线程退出方法
c#
public static void ParallelForWithInit() {
  Parallel.For<string>(0, 10, () => {
    // 开始工作
    // invoked once for each thread
    Log($"Init thread");
    return $"t{Thread.CurrentThread.ManagedThreadId}";
  },
  (i, pls, str1) => {
    // 迭代
    // invoked for each member
    Log($"finally {str1}");
    Task.Delay(10).Wait();
    return $"i {i}";
  },
  (str1) => {
    // 收尾工作
    // final action on each thread
    Log($"finally {str1}");
  });
}

21.2.4 使用 Parallel.ForEach()方法循环

c#
public static void ParallelForEach() {
  string [] data = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"};
  ParallelLoopResult result = Parallel.ForEach<string>(data, a => {
    Console.WriteLine(s);
  });

  // ParallelLoopState版本
  ParallelLoopResult result = Parallel.ForEach<string>(data, (a, pls, l) => {
    Console.WriteLine($"{s} {l}");
  });
}

21.2.5 通过 Parallel.Invoke()方法调用多个方法

如果多个任务将并行运行,就可以使用ParallelInvoke()方法,它提供了任务并行性模式。Parallel.Invoke()方法允许传递一个 Action 委托的数组,在其中可以指定将运行的方法

c#
public static void ParallelInvoke() {
  Parallel.Invoke(Foo, Bar);
}

public static void Foo => Console.WriteLine("foo");
public static void Bar => Console.WriteLine("bar");

21.3 任务

c#
public static void TaskMethod(object o) {
  Log(o?.ToString());
}

private static object s_logLock = new object();
public static void Log(string title) {
  lock(s_logLock) {
    Console.WriteLine(title);
    Console.WriteLine($"Task id: {Task.CurrentId?.ToString() ?? "no task"}, " +
      $"thread: {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"is pooled thread: " +
      $"{Thread.CurrentThread.IsThreadPoolThread}");
    Console.WriteLine($"is background thread: " +
      $"{Thread.CurrentThread.IsBackground}");
    Console.WriteLine();
  }
}

21.3.1 启动任务

  1. 使用线程池的任务
  • 第一种方式是使用实例化的 TaskFactory 类,在其中把 TaskMethod 方法传递给 StautNew 方法,就会立即启动任务。
  • 第二种方式是使用 Task 类的静态属性 Factory 来访问 TaskFactory,以及调用StartNew()方法。它与第一种方式很类似,也使用了工厂,但是对工厂创建的控制则没有那么全面。
  • 第三种方式是使用 Task 类的构造函数。实例化 Task 对象时,任务不会立即运行,而是指定 Created 状态。接着调用 Task 类的Start()方法,来启动任务。
  • 第四种方式调用 Task 类的Run()方法,立即启动任务

示例:

c#
public void TasksUsingThreadPoo1() {
  // 1.
  var tf = new TaskFactory();
  Task t1 = tf.StartNew(TaskMethod, "using a task factory");
  // 2.
  Task t2 = Task.Factory.StartNew(TaskMethod, "factory via a task");
  // 3.
  var t3 = new Task(TaskMethod, "using a task constructor and Start");
  t3.Start();
  // 4.
  Task t4 = Task.Run(() => TaskMethod ("using the Run method")); }
}
  1. 同步任务
c#
private static void RunSynchronousTask() {
  TaskMethod("just the main thread");
  var t1 = new Task(TaskMethod, "run sync");
  t1.RunSynchronously();
}
  1. 使用单独线程的任务

如果任务的代码将长时间运行,就应该使用TaskCreationOptions.LongRunning告诉任务调度器创建一个新线程,而不是使用线程池中的线程。

  • 这个线程不被线程池管理。相反对于线程池管理的线程,任务调度器在调度时会去查看状态,来复用线程,对于需要长时间运行的任务,这是没有意义的工作
c#
public static void LongRunningTask() {
  var t1 = new Task(TaskMethod, "long running", TaskCreationOptions.LongRunning);
  t1.start();
}

21.3.2 Future--任务的结果

// TODO:

Updated at: