【C#】CSharp
NOTE
C# 学习笔记,参考书: C#高级编程
3. 对象和类型
- 类和结构的区别
- 类成员
- 表达式体成员
- 按值和按引用传递参数
- 方法重载
- 构造函数和静态构造函数
- 只读字段
- 枚举
- 部分类
- 静态类
- object 类,其他类型都从该类派生而来
3.1 创建及使用类
3.2 类和结构
// 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:不能够着之外的地方被赋值
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 属性
class PhoneCustomer {
private string _firstName;
// 属性访问器
public string FirstName {
get {
return _firstName;
}
set {
_firstName = value;
}
}
}
1.具有表达式体的属性访问器
private string _firstName;
public string FirstName {
get => return _firstName;
set => _firstName = value;
}
2.自动实现的属性
如果属性的 set 和 get 访问器中没有任何逻辑,就可以使用自动实现的属性 不需要声明私有字段。编译器会自动创建它。使用自动实现的属性,就不能直接访问字段,因为不知道编 译器生成的名称。
// 简写:编译器会去生成随机名的私有属性,也不能直接访问
public int Age { get; set; }
// 简写 + 初始化
// 属性初始化器
public int Age ( get; set; } = 42;
3.属性的访问修饰符
// 权限修饰
public string Name {
get => return _name;
private set => _name = value;
}
// 权限修饰 简写
public string Name { get; private set; }
4.只读属性
// 省略set 访问器,就可以创建只读属性
private readonly string _name;
public string Name {
get => _name;
}
5.自动实现的只读属性
// 属性初始化器 初始化只读属性
public string Id { get; } = Guid.NewGuid().ToString();
public class person {
// 只读属性也可以显式地在构造函数中初始化
public Person(string name) = Name = name;
public String Name { get; }
}
6.表达式体属性 从 C#6 开始,只有 get 访问器的属性可以使用表达式体属性实现。
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 且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的 变量。
var caption = new {
FirstName = "James",
MiddleName = "T",
LastName = "Kirk"
};
var doctor = new {
FirstName = "Leonard",
MiddleName = string.Empty,
LastName = "McCoy"
};
// 有相同匿名属性的匿名对象可以赋值
caption = doctor;
如果所设置的值来自于另一个对象,则可以推断匿名类型成员的名称。这样,就可以简化初始化器。
// caption的属性来自person;赋值后caption会具有FirstName、MiddleName 和LastName属性
var caption = new {
person.FirstName,
person.MiddleName,
person.LastName
}
3.3.5 方法
- 方法的声明
// Syntax:
[modifiers] return_type MethodName([parameters]) {
// Method body
}
// Example:
public bol IsSquare(Rectangle rect) {
return (rect.Height = = rect.Width);
}
- 表达式体方法
public bol IsSquare(Rectangle rect) => rect.Height = = rect.Width;
- 调用方法
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.方法的重载
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
}
}
- 命名的参数
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.可选参数
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); // 指定后面的某个参数
- 个数可变的参数
使用可选参数,可以定义数量可变的参数。params 关键字
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 构造函数
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.表达式体和构造函数
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.从构造函数中调用其他构造函
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#的 一个特征是也可以给类编写无参数的静态构造函数。这种构造函数只执行一次,而前面的构造函数是 实例构造函数,只要创建类的对象,就会执行它。
class MyClass {
static MyClass() {
// initialization code
}
// rest of class definition
}
示例:
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 结构
结构是值类型,不是引用类型。它们存储在栈中或存储为内联,其生存期的限制与简单的数据类型一样。
- 结构不支持继承
- 对于结构,构造函数的工作方式有一些区别。如果没有提供默认的构造函数,编译器会自动提供一个,把成员初始化为其默认值
- 使用结构,可以指定字段如何在内存中布局
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#有一个解决方案:可空类型。可空类型是可以为空的值类型
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 枚举类型
枚举是一个值类型,包含一组命名的常量
public enum color {
Red,
Green,
Blue
}
private static vodi colorsamples() {
Color c = Color.Red;
Console.WriteLine(c);
}
默认情况下,enum 的类型是 int。这个基本类型可以改为其他整数类型(byte、short、int、带符号的 long 和无符号变量)。命名常量的值从。开始递增
public enum Color : short {
Red = 1,
Green = 2,
Blue = 3
}
// 枚举和其它类型强制转换
Color c2 = (Color)2;
short number = (short)c2;
还可以使用 enum
类型把多个选项分配给一个变量,而不仅仅是一个枚举常量。为此,分配给常量的值必须是不同的位,Flags
属性需要用枚举设置。 枚举类型 DaysOfWeek
为每天定义了不同的值。要设置不同的位,可以使用用 0x
前缀指定的十六进制值轻松地完成,
[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
设置不同的位,也可以结合单个位来包括多个值
[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: 返回枚举值的一个数组
// 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 扩展方法
扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中
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 运算符
- 类型信息运算符:
sizeof
、is
、typeof
、as
- 溢出异常控制运算符:
check
、unchecked
- 空合并运算符:
??
- 标识符的名称运算符:
nameof
6.2.1 运算符的简化操作
8. 委托、lambda 表达式和事件
8.1 引用方法
委托是寻址方法的.NET 版本。在 C++中,函数指针只不过是 一个指向内存位置的指针,它不是类型安全的。我们无法判断这个指针实际指向什么,参数和返回类型等项就更无从知晓了
而.NET 委托完全不同;委托是类型安全的类,它定义了返回类型和参数的类型。委托类不仅包含对方法的引用,也可以包含对多个方法的引用。 lambda 表达式与委托直接相关。当参数是委托类型时,就可以使用 lambda 表达式实现委托引用的方法。
8.2 委托
8.2.1 声明委托
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 示例:
private delegate string GetAString();
public static void Main() {
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine($"String is {firstStringMethod()}");
}
// 初始化
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 个参数的方法
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 多播委托
委托可以存放多个函数;多播表示会调用多个函数指针
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 匿名方法
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 变量的方法的实现代码;和委托结合
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 事件发布程序
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 解释:
public event EventHandler<CarInfoEventArgs> NewCarInfo;
// 委托EventHandler<TEventArgs >的定义如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs: EventArgs
8.4.2 事件侦听器
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
类构建大的字符串格式化表达式:接又
IFormatProvider
和IFoumattable
来处理正则表达式:
Span:.NETCore 提供了泛型 Span 结构,它允许快速访问内存。
Span<T>
允许访问字符串的切片,而不需要复制字符串
9.1 System.String 类
// 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:指定字符串在分配的内存中的最大长度
// 指定内容
var sb = new StringBuilder("Hello") ;
// 指定容量
var sb = new StringBuilder(20) ;
常用方法:
Append()
:AppendFormat()
Insert()
Remove()
Replace()
ToString()
9.2 字符串格式
// 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 ());
转义花括号
string s ="Hello";
Console.WriteLine ($"((s)} displays the value of s: (s)");
9.4 字符串和 Span
今天的编程代码通常处理需要操作的长字符串。例如,WebAPI 以 JSON 或 XML 格式返回一个长字符串。将如此大的字符串分割成许多更小的字符串,意味着创建了许多对象,而垃圾收集器不再需要这些字符串时,需要做很多事情来释放这些字符串所占的内存。 .NETCore 有一个新的方法: Span<T>
类型。该类型引用数组的一个切片,而不需要复制它的内容。同样,Span<T>
可以用来引用一个字符串的片段,而不需要复制原始内容
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.Collections
和 System.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 增加了两个关键字来简化异步编程 : async
和 await
3 种不同模式的异步编程:
- 异步模式
- 基于事件的异步模式
- 基于任务的异步模式(Task-based AsynchronousPatter, TAP), 用 async 和 await 来实现
15.2 异步编程的.NET 历史
- .NETFramework 1.0 开始提供了异步特性。.NETFramework 的许多类都实现了一个或者多个异步模式
- .NETFramework 2.0 推出了基于事件的异步模式
- .NETFramewouk 4.5 中,推出基于任务的异步模式(TAP)。基于 Task 类型,并通过
async
和await
关键字使用编译器功能
15.2.1 同步调用
会卡信执行流程
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,用于验证调用是否已经完成,并且一直等到方法的执行结束。
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 去显示。
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 类型
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
static async Task Main() {
SynchronizedAPI();
AsynchronousPattern();
EventBasedAsyncPattern();
await TaskBasedAsyncPatternAsync();
Console.ReadLine();
}
15.3 异步编程的基础
async 和 await 关键字只是编译器功能。编译器会用 Task 类创建代码
- 编译器用 async 和 await 关键字能做什么
- 如何采用简单的方式创建异步方法
- 如何并行调用多个异步方法
- 以及如何修改已经实现异步模式的类,以使用新的关键字
任务 id 和线程 id
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 创建任务
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 调用异步方法
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 接又。此方法在任务完成时调用。
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 属性可以访问任务返回的结果:
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 使用多个异步方法
- 顺序调用
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}")
}
- 组合器
Task 类定义了 When All 和 When Any 组合器。从 WhenAII 方法返回的 Task ,是在所有传入方法的任务都完 成了才会返回 Task。从 WhenAny 方法返回的 Task,是在其中一个传入方法的任务完成了就会返回 Task。
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 返回的结果
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 错误处理
static async Task ThrowAfter(int ms, string message) {
await Task.Delay(ms);
throw new Exception(message);
}
如果调用异步方法,并且没有等待,就会捕获不到异常
private void DontHanle() {
try {
// NOte: 调用异步没有await会捕获不到异常
Throwafter(2000, "first");
} catch (Exception ex) {
Console.WriteLine(ex.message);
}
}
15.4.1 异步方法的异常处理
private void HanleOneError() {
try {
await Throwafter(2000, "first");
} catch (Exception ex) {
Console.WriteLine(ex.message);
}
}
15.4.2 多个异步方法的异常处理
如果调用两个异步方法,每个都会抛出异常,该如何处理呢?
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 运行结束
- 但异常捕获只会捕获到一个异常
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 信息
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 解决方案文件
解决方案文件:组合多个项目时使用,通常是一个程序加上一个或多个库
# 创建解决方案
$ dotnet new sin
# 添加项目
$ dotnet sln add Simplelib/Simplelib.csproj
19.3.4 引用项目
$ dotnet add reference ..\Simplelib\Simplelib.csproj
ItemGroup/ProjectReference
19.3.5 引用 NuGet 包
$ 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 输出代码
public static void Log(string prefix) =>
Console.WriteLine($"{prefix}, task: {Task.CurrentId}, " + $"thread: {Thread.CurrentThread.ManagedThreadId}");
Parallel.For
和Task.Delay(10).Wait()
示例
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.For
和async/await
示例
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()
方法,以影响循环的结果。
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>
; 在该示例中,接收一个字符串, 这个方法仅对于每个线程调用一次,这是一个线程退出方法
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()方法循环
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 委托的数组,在其中可以指定将运行的方法
public static void ParallelInvoke() {
Parallel.Invoke(Foo, Bar);
}
public static void Foo => Console.WriteLine("foo");
public static void Bar => Console.WriteLine("bar");
21.3 任务
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 启动任务
- 使用线程池的任务
- 第一种方式是使用实例化的 TaskFactory 类,在其中把 TaskMethod 方法传递给 StautNew 方法,就会立即启动任务。
- 第二种方式是使用 Task 类的静态属性 Factory 来访问 TaskFactory,以及调用
StartNew()
方法。它与第一种方式很类似,也使用了工厂,但是对工厂创建的控制则没有那么全面。 - 第三种方式是使用 Task 类的构造函数。实例化 Task 对象时,任务不会立即运行,而是指定 Created 状态。接着调用 Task 类的
Start()
方法,来启动任务。 - 第四种方式调用 Task 类的
Run()
方法,立即启动任务
示例:
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")); }
}
- 同步任务
private static void RunSynchronousTask() {
TaskMethod("just the main thread");
var t1 = new Task(TaskMethod, "run sync");
t1.RunSynchronously();
}
- 使用单独线程的任务
如果任务的代码将长时间运行,就应该使用TaskCreationOptions.LongRunning
告诉任务调度器创建一个新线程,而不是使用线程池中的线程。
- 这个线程不被线程池管理。相反对于线程池管理的线程,任务调度器在调度时会去查看状态,来复用线程,对于需要长时间运行的任务,这是没有意义的工作
public static void LongRunningTask() {
var t1 = new Task(TaskMethod, "long running", TaskCreationOptions.LongRunning);
t1.start();
}
21.3.2 Future--任务的结果
// TODO: