C#语法基础详解(万字总结)
文章目录
- **参考书籍:`C#7.0 核心技术指南`**
- 类型
- 高级特性
- 框架基础
- 集合
- Linq
- Linq运算符
- 1. 过滤运算符(Filtering Operators)
- 2. 投影运算符(Projection Operators)
- 3. 排序运算符(Sorting Operators)
- 4. 连接运算符(Join Operators)
- 5. 分组运算符(Grouping Operators)
- 6. 合运算符(Set Operators)
- 7. 元素运算符(Element Operators)
- 8. 聚合运算符(Aggregate Operators)
- 9. 转换运算符(Conversion Operators)
- 10. 生成运算符(Generation Operators)
- 11. 量词运算符(Quantifier Operators)
- 12. 分区运算符(Partitioning Operators)
- 对象销毁与垃圾回收
- 并发与异步
- 流与I/O
- *网络
- 序列化
- 反射和元数据
- 高级线程处理
参考书籍:C#7.0 核心技术指南
类型
类
非嵌套的类修饰符
public
、internal
、abstract
、sealed
、static
、unsafe
、partial
字段
class User{
string name;
public int Age =10;
}
字段可以用以下修饰符进行修饰
- 静态修饰符:
static
- 访问权限修饰符:
public
,internal
,private
,protected
- 继承修饰符:
new
- 不安全代码修饰符:
unsafe
- 只读修饰符:
readonly
- 线程访问修饰符:
volatile
**readonly只读修饰符:**只能在声明或构造器中赋值
重载
void Foo(int x){...}
void Foo(double x){...}
void Foo(int x,double y){...}
void Foo(string x,int y){...}
若返回类型不同则不是重载,类型中不可以使用。
按值传递和按引用传递
void Foo(ref int x)//或Foo(out int x)
void Foo(out int x) //error compile-time
void Foo(int x)
局部方法
局部方法不能使用static修饰,如果父方法是静态的那么局部方法也是静态的
void WriteCubes(){
Console.WriteLine(Cube(3));
int Cube (int value)=>value*value*value;
}
重载构造器
public class Person{
public int Age ;
public string Name;
public Person(int age){Age=age;}
public Person (int Age,string name):this(age){Name=name;}
}
当构造器调用另一个构造器的时候,被调用的构造器先执行
解构器
class DeconTest
{
private readonly int a1 = 10, a2 = 10, a3 = 10;
public DeconTest(int a1, int a2, int a3)
{
this.a1 = a1;
this.a2 = a2;
this.a3 = a3;
}
public void Deconstruct(out int A1, out int A2, out int A3)
{
A1 = a1;
A2 = a2;
A3 = a3;
}
public void Deconstruct(out int A1, out int A2)
{
A1 = a1;
A2 = a2;
}
}
DeconTest deconTest = new DeconTest(1, 2, 3);
var (b1, b2) = deconTest;
var (c1, c2, c3) = deconTest;
Console.WriteLine($"{b1} - {b2} - {c1} - {c2} - {c3}");
解构器是为了更好的拿到类中的属性
对象初始化器
namespace Init;
class InitTest
{
public int A1 = 1, A2 = 2;
public InitTest()
{
}
public InitTest(int a1)
{
A1 = a1;
}
}
InitTest initTest = new InitTest() { A1 = 10, A2 = 5 };
InitTest initTest2 = new InitTest(11) { A2 = 5 };
Console.WriteLine($"{initTest.A1}-{initTest.A2}-{initTest2.A1}-{initTest2.A2}");
属性
属性有get和set方法
public class Stock{
// The private "backing" field// The public property
decimal currentPrice;
public decimal CurrentPrice{
get{return currentPrice;}
set{currentPrice=value;}
}
}
只读属性
public class Stock{
// The private "backing" field// The public property
decimal currentPrice;
public decimal CurrentPrice{
get{return currentPrice;}
}
}
表达式属性(只读属性才可以)
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
自动属性
public class Stock{
public decimal CurrentPrice{get;set;}
}
属性初始化器
public int Maximum{get;}=999;
public currentPrice{get;set;}=123;
索引器
namespace Index;
public class IndexerTest
{
string[] words = "czm is best".Split();
public string this[int wordsNum]
{
get { return words[wordsNum]; }
set { words[wordsNum] = value; }
}
}
IndexerTest indexerTest = new IndexerTest();
Console.WriteLine(indexerTest[0]);
indexerTest[2] = "jamin";
Console.WriteLine(indexerTest[2]);
静态构造器
静态构造器只执行一次,不是每个实例执行一次。
nameof运算符
nameof运算符返回任意符号的字符串的名称(类型、成员、变量等)
string name =nameof(StringBuilder.Length)//name is Length
继承
类型转换和引用转换
as运算符
Animal animal = new Dog();
Dog dog = animal as Dog;
if (dog != null)
{
dog.Bark();
}
使用as运算符若转换失败返回Null而不报错
is运算符
Animal animal = new Dog();
if (dog is animal)
{
dog.Bark();
}
is运算符检验转换是否成功
is与模式变量
if(a is Animal animal){Console.WriteLine(s.name)}
虚函数成员
提供特定实现的子类可以重写标识为virtual的函数。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("This animal makes a sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("The cat meows");
}
}
抽象类和抽象成员
声明为抽象的类不能够实例化。
抽象成员不提供默认的实现,其实现必须由子类提供。除非子类也为抽象类。
new和重写
New
public class BaseClass
{
public void MyMethod()
{
Console.WriteLine("BaseClass.MyMethod()");
}
}
public class DerivedClass : BaseClass
{
new public void MyMethod()
{
Console.WriteLine("DerivedClass.MyMethod()");
}
}
BaseClass obj = new DerivedClass();
obj.MyMethod(); // 输出 "BaseClass.MyMethod()"
重写
public class BaseClass
{
public virtual void MyMethod()
{
Console.WriteLine("BaseClass.MyMethod()");
}
}
public class DerivedClass : BaseClass
{
public override void MyMethod()
{
Console.WriteLine("DerivedClass.MyMethod()");
}
}
BaseClass obj = new DerivedClass();
obj.MyMethod(); // 输出 "DerivedClass.MyMethod()"
base关键字
Public class House :Asset{
public override decimal Liability =>base.Liability +Mortgage;
}
通过base访问基类的属性。
构造器与继承
public class BaseClass
{
public BaseClass()
{
Console.WriteLine("Base class constructor called.");
}
}
public class DerivedClass : BaseClass
{
public DerivedClass() : base()
{
Console.WriteLine("Derived class constructor called.");
}
}
class Inherit
{
public Inherit(int a)
{
Console.WriteLine("base" + a);
}
}
class SubInherit : Inherit
{
public SubInherit() : base(10)
{
Console.WriteLine("Sub");
}
}
若基类中没有无参构造函数则派生类必须带有一个显式调用基类的一个带参数的构造函数。
object类型
装箱和拆箱
装箱:
将值类型实例转换为引用类型实例
int x=9;
object obj =x;
拆箱:
将引用类型转换为值类型(需要显式的类型转换)
int y =(int)obj;
装箱是把值类型的实例复制到新对象中,而拆箱是把对象的内容复制回值类型的实例中。下面的示例修改了i的值,但并不会改变它先前装箱时复制的值:
int i= 3; object boxed =i; i =5; Console.WriteLine(boxed);//3
GetType和typeof
在类型实例上调用GetType方法(运行时)
在类型名称上使用typeof运算符(编译时)
public class Point {public int x,y;}
class Test{
static void Main(){
Point p=new Point();
Console.WriteLine(p.GetType().Name);
Console.WriteLine(typeof(Point).Name);
Console.WriteLine(typeof(p.x.GetType().FullName);
}
}
结构体
值类型 派生自System.ValueType
using System;
using System.Text;
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};
public class testStructure
{
public static void Main(string[] args)
{
Books Book1; /* 声明 Book1,类型为 Books */
Books Book2; /* 声明 Book2,类型为 Books */
/* book 1 详述 */
Book1.title = "C Programming";
Book1.author = "Nuha Ali";
Book1.subject = "C Programming Tutorial";
Book1.book_id = 6495407;
/* 打印 Book1 信息 */
Console.WriteLine( "Book 1 title : {0}", Book1.title);
Console.WriteLine("Book 1 author : {0}", Book1.author);
Console.WriteLine("Book 1 subject : {0}", Book1.subject);
Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);
}
}
友元程序集
…
接口
接口不提供成员的实现,因为它所有的成员都是隐式抽象的。
接口成员总是隐式public访问权限
接口实现类是internal访问权限但仍可以通过接口作为public去访问成员
public interface Ienumerator{
bool MoveNext();
object Current{get;}
void Reset();
}
internal class countdown :Ienumerator{
int count =11;
public bool MoveNext=>count-- >0;
public object Current =>count;
public void Reset(){throw new Exception();}
}
Ienumerator e=new Countdown();
Console.Write(e.Current);
扩展接口
接口可以从其他接口派生
public interface Iundoable{void Undo();}
public interface IRedoable : Iundoable{void Redo();}
Iredoable继承了Iundoable接口的所有成员。
显式接口实现
interface I1{void Foo();}
interface I2{int Foo();}
public class Widget:I1,I2{
public void Foo(){
Console.WriteLine("I1.Foo");
}
int I2.Foo(){
Console.WriteLine("I2.Foo");
return 27;
}
}
虚方法实现接口
默认情况下,隐式实现的接口成员是密封的。为了重写,必须在基类中将其标识为virtual或者abstract。
虚函数常常被用在模板方法设计模式中,在这个模式中,一个父类定义了一个方法来完成一个特定的工作流程,而将这个工作流程中的某些步骤延迟到其子类中实现。这些可以被子类定制的步骤通常被定义为虚函数。
public interface IUndoable{void Undo();}
public class TextBox:IUndoable{
public virtual void Undo()=>Console.WriteLine("TextBox.Undo");
}
public class RichTextBox:TextBox{
public override void Undo()=>Console.WriteLine("RichTextBox.Undo");
}
调用时不管从基类还是接口中调用接口成员都是 子类的实现 “RichTextBox.Undo”
枚举类型
public enum BorderSide{Left,Right,Top,Bottom}//默认为0,1,2,3
public enum BorderSide{Left=1,Right=5,Top=10,Bottom=12}//指定值
当指定部分枚举的值时,会在有值的枚举值上递增。
枚举类型转换
int i=(int) BorderSide.Left;
BorderSide side =(BorderSide) i;
标志枚举类型
BorderSide leftRight=BorderSide.Left | BorderSide.Right;
if((leftRight & BorderSide.Left)!=0)
Console.WriteLine("包含 Left");
**注:**枚举可以与整形参与运算,但两个枚举不可以做加法。
嵌套类型
public class TopLevel
{
public class nested { }
public enum Color { red, green, blue }
}
TopLevel.Color color = TopLevel.Color.red;
-
可以访问包含它的外层类型中的私有成员,以及外层类所能够访问的所有内容
public class TopLevel { static int x; class Nested{ static void Foo(){ Console.WriteLine(TopLevel.x);} } }
-
可以在声明上使用所有的访问权限修饰符,而不限于public和internal.
public class TopLevel
{
protected class Nested {}
}
public class SubTopLevel :TopLevelstatic{
void Foo(){ new TopLevel.Nested();}
}
- 嵌套类型的默认可访问性是private而不是internal.
- 从外层类以外访问嵌套类型,需要使用外层类名称进行限定(就像访问静态成员一样)。
public class TopLevel
{
public class nested { }
}
class Test{
TopLevel.Nested n;
}
泛型
public class Stack<T>
{
int position;
T[]data = new T[100];
public void Push(T obj)=> data[position++]= obj;
public TPop()=>data[--position];
}
var stack =new Stack<int>();
stack.Push(5);
stack.Push(10);
int x =stack.Pop();//x is 10
int y =stack.Pop();//y is 5
泛型方法
static void Swap<T>(ref T a,ref T b){
T temp =a;
a=b;
b=temp;
}
int x=5;
int y=10;
Swap(ref x,ref y);
//或者Swap<int>(ref x,ref y)显式指出泛型的类型
声明类型参数
public struct Nullable<T>{
public T Value{get;}
}
泛型或方法可以有多个参数,例如:
class Dictionary<TKey,TValue>{...}
可以用以下方式实例化:
Dictionary<int,string>myDic =new Dictionary<int,string>();
typeof 和未绑定泛型类型
Type t = typeof(Dictionary<,>);
Console.WriteLine(t.Name);
泛型的约束与继承
在下面的例子中GenericClass<T,U>的T要求派生自(或者本身就是)SomeClass并且实现 Interface1;要求U提供无参数构造器。
class GenericClass<T,U> where T : SpmeClass,Interface1 where U:new()
继承
class Stack<T>{...}
class SpecialStack<T>:Stack<T>{...}
静态数据
class Bob<T>{public static int Count;}
class Test{
static void Main(){
Console.WriteLine(++Bob<int>.Count); //1
Console.WriteLine++Bob<int>.Count);//2
Console.WriteLine++Bob<string>.Count);//1
Console.WriteLine++Bob<obect>.Count);//1
}
协变与逆变
…
高级特性
委托
delegate int Transformer (int x);
class Test{
static void Main(){
Transformer t=Square;
int result=t(3);
Console.WriteLine(result);
}
static int Square(int x)=>x*x;
}
Transformer t=Square;
int result =t(3);//t(3)是t.
委托实例: 调用者调用委托,委托调用目标方法。
委托书写插件
public delegate int Transformer(int x);
class Util{
public static void Transform (int[] values,Transformer t){
for(int i=0;i<values.Length;i++)
values[i]=t(Values[i]);
}
}
class Test{
static void Main(){
int[] values={1,2,3};
Util.Transform(values,Square);
foreach(int i in values)
Console.Write(i+" ");
}
static int Square(int x)=>x*x;
}
多播委托
所有的委托实例都拥有多播能力。这意味着一个委托实例可以引用一个目标方法,也可以引用一组目标方法。委托可以使用+和+=运算符联结多个委托实例。例如:
SomeDelegate d=SomeMethod1;d +=SomeMethod2:
public delegate void ProgressReporter(int percentComplete);
public class Util{
public static void HardWork(ProgressReporter p){
for(int i=0;i<10;i++){
p(i*10);
System.Threading.Thread.Sleep(100);
}
}
}
class Test{
static void Main(){
ProgressReporter p=WriteOrigressToConsole;
p += WriteProgressTofile;
Util.HardWork(p);
}
static void WriteProgressToConsole(int percentComplete)
=>Console.WriteLine(percentComplete);
static void WriteProgressToFile(int percentComplete)
=>System.IO.File.WriteAllText("progress.txt",percentComplete.ToString());
}
实例目标方法和静态目标方法
将一个实例方法赋值给委托对象时,后者不但要维护方法的引用,还需要维护方法所属的实例的引用。
ProgressReporterp=x.InstanceProgress;
public delegate void ProgressReporter(int percentComplete);
class Test{
static void Main(){
X x= new X();
ProgressReporterp=x.InstanceProgress;
p(99);// 99
Console.WriteLine(p.Target==x);// True
Console.WriteLine(p.Method);//Void InstanceProgress(Int32)
}
}
class X{
public void InstanceProgress(int percentComplete)
=>Console.WriteLine(percentComplete);
}
泛型委托
public delegate T Transformer<T>(T arg);
public class Util{
public static void Transform<T>(T[] values, Transformer<T> t){
for(int i=0;i<values.Length;i++)
values[i]=t(values[i]);
}
}
class Test{
static void Main(){
int[]values={1,2,3 };
Util.Transform(values,Square);// Hook in Square
foreach(int i in values)
Console.Write(i+" ");//14
}
static int Square(intx)=>x*x;
}
Func和Action
delegate TResult Func <out TResult>();
delegate TResult Func <in T,out TResult>(T arg);
delegate TResult Func <in T1,in T2,out TResult> (T1 arg1,T2 arg2);
out TResult 表示返回值 。(协变)
in T 标识参数类型。(逆变)
向上转型是协变,向下转型是逆变。
使用BeginInvoke(null, null);可以开启新线程
Action action = () =>
{
Console.WriteLine("Hello from another thread.");
};
// 运行在当前线程
action();
// 运行在新的线程
action.BeginInvoke(null, null);
参数兼容与返回类型的兼容
参数兼容
delegate void StringAction(string s);
class Test{
static void Main(){
StringAction sa=newstringAction(ActOnObject);
sa("hello");// hello
}
static void ActOnObject (object o)=>Console.WriteLine(o);
}
返回类型兼容
delegate object ObjectRetriever();
class Test{
static void Main(){
ObjectRetriever o=new ObjectRetriever(RetrieverString);
object result =o();
Console.WriteLine(result);// hello
}
static string RetrieverString ()=>"hello";
}
*事件
响应式监听一个对象的改变
public delegate void PriceChangedHandler(decimal oldPrice decimal newPrice):
public class Stock{
public event PricechangedHandler PriceChanged;
}
标准事件模式
namespace DeleTest;
public class delegateTest
{
string symbol;
decimal price;
public delegateTest(string symbol)
{
this.symbol = symbol;
}
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
}
}
}
public class PriceChangedEventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
delegateTest delegateTest = new delegateTest("jamin");
delegateTest.Price = 27.10M;
delegateTest.PriceChanged += delegateTest_PriceChanged;
delegateTest.Price = 31.27M;
static void delegateTest_PriceChanged(object sender, PriceChangedEventArgs e)
{
if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
{
Console.WriteLine("Alert,10% stock price increase");
}
}
Lambad表达式
lambad表达式与委托一起使用
Func<string,string,int>totalLength=(s1,S2)=>s1.Length + s2.Length;
int total=totalLength("hello","world");
// total is 10;
显式指定Lambad参数类型
void Bar<T>(Action<T> a){}
Bar ((int x)=>Foo(x));
捕获变量
捕获的变量会在真正调用委托时赋值,而不是在捕获时赋值.
Action[]actions =new Action[3];
for(inti=0;i<3;i++)
actions [i]=>Console.Write(i);
foreach(Action ain actions) a(); ///333
只有在调用的时候委托才能看到值,此时i已经为3.
当变量为局部变量的时候可以此时可以捕获不同的变量
x Action[]actions =new Action[3];
for(int i=0;i<3;i++)
{
int loopScopedi=i;
actions [i]=>Console.Write(loopScopedi);
}
foreach(Action ain actions) a(); ///012
try语句和异常
异常筛选器
catch(WebException ex) when (ex.Status==WebExceptionStatus.Timeout){
...
}
catch(WebException ex) when (ex.Status==WebExceptionStatus.SendFailure){
...
}
使用when关键字后可以重复捕获同类型的异常,直至精准的异常类型
using语句
using(StreamReader reader =File.OpenText("file.txt")){
...
}
使用了using语句的非托管资源可以在using语句结束后自动释放资源
可枚举类型和迭代器
可枚举类型
枚举器(Enumerator)是一个只读的且只能在值序列上前移的游标。枚举器实现下面接口之一:
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator
class Enumerator{//Typically implements IEnumerator or IEnumerator<T>
public IteratorVariableType Current {get {...}}
public bool MoveNext(){...}
}
using(var enumerator="beer".GetEnumerator())
while(enumerator.MoveNext()){
var element=enumerator.Current;
Console.WriteLine(element);
}
foreach(char c in "beer")
Console.WriteLine(c);//高层次遍历,结果如上
迭代器
public static IEnumerable<int> Power(int number, int exponent)
{
int result = 1;
for (int i = 0; i < exponent; i++)
{
result = result * number;
yield return result;
}
}
可空类型
引用类型可以使用空引用表示一个不存在的值,然而值类型不能直接表示为null
string s=null;
int i = null;//error
int? i=null//OK ?表示可为空
Nullable 结构体
int? i = null;
Console.WriteLine(i== null);//true
=====
Nullable<int>i=new Nullable<int>();
Console.WriteLine(!i.HasValue); //true
//当HasValue为true时,GetValueOrDefault()会返回value
扩展方法
扩展方法循序在现有的类型上扩展新的方法而无需修改原始类型的定义。扩展方法是静态类的静态方法,而其中的第一个参数需要用this修饰符修饰,第一个参数就是需要扩展的类型。
public static class StringHelper
{
public static bool IsCapitalized(this string s){
if(string.IsNull0rEmpty(s)) return false;
return char.IsUpper(s[0]);
}
}
Console.WriteLine("Perth".IsCapitalized());
扩展方法链
public static class stringHelper{
public static string Pluralize(this string s){...}
public static string capitalize(this string s){...}
}
string x="sausage".Pluralize().Capitalize();
string y=stringHelper.Capitalize(StringHelper.Pluralize("sausage"));
匿名类型
匿名类型是一个由编译器临时创建来存储一组值的简单类。如果需要创建一个匿名类型,则可以使用new关键字,后面加上对象初始化器,指定该类型包含的属性和值。
var dude =new {Name="Bob",Age=23};//匿名类型只能通过var关键字引用
在同一个程序集内声明的两个匿名类型实例,如果它们的元素名称和类型是相同的,那么它们在内部就是相同的类型:
var a1=new{X=2,Y= 4}; var a2=new{X=2,Y=4}; Console.WriteLine(a1.GetType()== a2.GetType());// True
注:匿名方法重写了Equals方法
元组
元组是值类型。并且他们是可变的(可读可写)的元素。
var bob=("bob",11);
var joe=bob;
joe.Item1="joe";
Console.WriteLine(bob);//(bob,11)
Console.WriteLine(joe);//(joe,11)
//显式指定元组类型
(string,int) bob=("bob",11);
static(string,int)GetPerson()=>("bob",11);
static void Main(){
(string,int)person=GetPerson();//可以使用var
Console.WriteLine(person.Item1);
Console.WriteLine(person.Item2);
}
元组元素命名
var tuple =(Name:"Bob",Age:23);
Console.WriteLine(tuple.Name);
Console.WriteLine(tuple.Age);
=======
static(string Name,intAge)GetPerson()=>("Bob",23);
var Person =GetPerson();
Console.WriteLine(Person.Name);
Console.WriteLine(Person.Age);
**注:**即使使用了命名仍然可以使用Item进行访问
元组的解构
var bob =("Bob",23);
(string name,int age)= bob;
Console.WriteLine(name);
Console.WriteLine(age);
**注:**元组也重写了Equals方法。
*特性
特性类
特性是通过直接或者间接继承抽象类System.Attribute的方式定义的。如果要将个特性附加到一个代码元素中,那么就需要在该代码元素之前用方括号指定特性的类型名称。
动态绑定
动态绑定(dynamic binding)将绑定(binding)(即解析类型、成员和操作的过程)从编译时延迟到运行时。
dynamic d=GetSome0bject();
d.Quack();
不安全的代码和指针
指针基础
&, 取地址运算符返回指向某个变量地址的指针
*, 解引用(复引用)运算符返回指针指向地址的变量
-> 指针取成员运算符是一个快捷语法,其中x->y 等价于(*x).y
不安全的代码
使用unsafe关键字修饰类型、成员或语句块就可以在该范围内使用指针类型。
*预处理指令
XML文档
框架基础
字符串与文本处理
字符
静态方法 | 包含的字符 | 包含的Unicode分类 |
---|---|---|
IsLetter | A-Z\a-z和其他字母字符 | UpperCaseLetter LowerCaseLetter TitleCaseLetter ModifierLetter OtherLetter |
IsUpper | 大写字母 | UpperCaseLetter |
IsLower | 小写字母 | LowerCaseLetter |
IsDigit | 0-9 和其他字母表中的数字 | DecimalDigitNumber |
IsLetter0rDigit | 字母和数字 | (IsLetter, IsDigit) |
IsNumber | 所有数字以及 Unicode 分数和罗马数字符号 | DecimalDigitNumber LetterNumber OtherNumber |
IsSeparator | 空格与所有的 Unicode 分隔符 | LineSeparator ParagraphSeparator |
IsWhiteSpace | 所有的分隔符,以及\n、\r、\t、\f 和 \v | LineSeparator ParagraphSeparator |
IsPunctuation | 西方和其他字母表中的标点符号 | DashPunctuation Connector Punctuation InitialQuote PunctuationFinalOuotePunctuation |
IsSymbol | 大部分其他的可打印符号 | MathSymbol ModifierSymbol Othersymbol |
字符串
char[]ca ="Hello".ToCharArray();
strings=new string(ca);
字符串可以使用下标进行获取值
string str ="abcde" ;
char letter=str[1];
IndexOf和IndexOfAny、LastIndexOf
string str = "Hello, World!";
int index = str.IndexOf("World");
Console.WriteLine(index); // 输出 7
string s = "Hello, World!";
char[] charsToFind = { 'o', 'W', '!' };
int idx = s.IndexOfAny(charsToFind); // 返回 4,这是字符'o'、'W'、或 '!' 在字符串s中第一次出现的位置(从0开始计数)
string text = "Hello, World! World!";
int lastIndex = text.LastIndexOf("World"); //返回 13,这是子字符串"World"在字符串text中最后出现的位置(从0开始计数)。
string text = "Hello, World! World!";
char[] charsToFind = { 'o', 'W', '!' };
int idx = text.LastIndexOfAny(charsToFind); // 返回 18,这是字符'o'、'W'、或 '!' 在字符串 s 中最后出现的位置 (从0开始计数)
字符串处理
1、Substring
2、Insert
3、Remove
4、PadLeft和PadRight(将字符串填充为指定的长度)
5、TrimStart和TrimEnd(消除空格)
6、Split(字符串分割)
7、Concat(字符串拼接)
String.Format与组合格式字符串
string composite="It's{o}degrees in{1}on this{2} morning";
string s=string.Format(composite,35,"Perth",DateTime.Now.DayOfWeek);
//s == "It's 35 degrees in Perth on this Friday morning
使用 $ 插值字符串
int age =10;
string name ="Jamin";
Console.WriteLine($"{name}-{age}");
字符串的比较
Compare
StringBuilder
StringBuilder类(System.Text命名空间)表示一个可变(可编辑)的字符串。StringBuilder可以直接进行子字符串的Append、Insert、Remove和Replace 而不需要替换整个StringBuilder。
**注:**string会替换整个字符串,Stringbuilder会自动调整他的内部大小至最大的容量
将StringBuilder的Length属性设置为0并不会减小其内部容量因此,如果之前StringBuilder已经包含了一百万个字符,则它在Length设置为0后仍然占用2MB的内存。因此,如果希望释放这些内存,则必须新建一个StringBuilder,然后将旧的对象清除出作用域(从而可以被垃圾回收)。
日期和时间
TimeSpan
创建 TimeSpan的方法有三种:
- 通过它的一个构造器
- 通过调用其中一个静态From…·方法
- 通过两个 DateTime 相减得到
public TimeSpan(int days,int hours,int minutes,int seconds,int milliseconds);
public static TimeSpan FromDays(double value);
public static TimeSpan FromHours(double value);
public static TimeSpan FromMinutes(double value);
public static TimeSpan FromSeconds(double value);
public static TimeSpan FromMilliseconds(double value);
TimeSpan nearlyTenDays=TimeSpan.FromDays(10)- TimeSpan.FromSeconds(1);
DateTime和DateTimeOffset
DateTime和DateTimeOffset都是表示日期或者时间的不可变结构体。它们的最小单位均为100纳秒,而且值的范围为从0001年到9999年.
**注:**DateTimeOffset主要处理时区
Console.WriteLine(DateTime.Now.ToString("yyy-MM-dd hh:mm:ss"));
格式化和解析
ToString和Parse
string s=true.ToString();//s ="True"
bool b=bool.Parse(s);//b=true
int i;
bool failure =int.TryParse("qwerty",out i);
bool success =int.TryParse("123”,out i);
格式提供器
Console.WriteLine(3.ToString("C", NumberFormatInfo.CurrentInfo));//¥3.00
Convert
Convert和Parse都是用来处理类型转换的,但是Convert会将“ ”和Null转换为类型的默认值
Convert.ToInt32("")
将会返回0。
*全球化
GUID
Guid结构体表示一个全局唯一标识符:一个在生成时就几乎可以肯定为全世界唯一的16字节值。Guid在应用程序和数据库中通常作为各种排序的健,而Guid可表示的值总共有 2128 或3.4x1018个。
Guid g= Guid.NewGuid();
Console.WriteLine(g.Tostring());
相等比较
Equals虚方法
Equals会比较值而==比较引用类型
如果重写Equals也要重写getHashCode方法。
实用类
Environment
- 文件和文件夹:CurrentDirectory、SystemDirectory、CommandLine
- 计算机和操作系统:MachineName、ProcessorCount、0SVersion、NewLine
- 用户登录:UserName、UserInteractive、UserDomainName
- 诊断信息:TickCount、StackTrace、Workingset、Version
Process
Process可以启动一个新线程
Process.Start("notepad.exe");
System.Diagnostics.Process.Start("D:\\Notepad++\\notepad++.exe", "D:\\Notepad++\\readme.txt");
集合
枚举
IEnumerable和IEnumerator
IEnumerator
public interface Inumerator
{
bool MoveNext();//游标
object Current { get;}//获取当前值
void Reset();//将当前位置移回起点
}
用法
string s="Hello";
//Because string implements IEnumerable, we can call GetEnumerator():
IEnumerator rator=s.GetEnumerator;
while(rator.MoveNext()){
char c=(char)rator.Current;
Console.Write(c+".");
}
//H.e.l.l.o.
一般在调用枚举器的时候使用foreach循环获取值。
实现枚举接口
- 为了支持 foreach语句
- 为了与任何标准集合进行互操作
- 为了达到一个成熟的集合接口的要求
- 为了支持集合初始化器
ICollection和IList接口
ICollection和ICollection
ICollection标准集合接口可以对其中的对象进行计数。它可以确定集合大小(Count),确定集合中是否存在某个元素(Contains),将集合复制到一个数组(ToArray)以及确定集合是否为只读(IsReadonly)。对于可写集合,还可以对集合元素进行添加(Add)、删除(Remove)以及清空(Clear)操作。由于它实现了 IEnumerable,因此也可以通过 foreach 语句进行遍历。
public interface ICollection<T>:IEnumerable<T>,IEnumerable{
int Count{get;}
bool Contains(T item);
void CopyTo(T[] array,int arrayIndex);
bool IsReadOnly{get;}
void Add(T item);
bool Remove(T item);
void Clear();
}
非泛型的 ICollection也提供了计数的功能,但是它并不支持修改或检查集合元素的功能:
IList和IList
IList是按照位置对集合进行索引的标准接口除了从ICollection和IEnumerable继承的功能之外。它还可以按位置(通过索引器)读写元素,并在特定位置插入/删除元素。
public interface IList<T>: ICollection<T>,IEnumerable<T>,IEnumerable{
T this [int index]{get;set;}
int IndexOf(T item);
void Insert(int index,T item);
void RemoveAt (int index);
}
IList继承了非泛型的集合,最主要的区别是Add的返回值为int而泛型的Add返回值为void。
IReadOnlyList
IReadonlyList。这个接口本身很有用,并可以看作 IList的缩减版本,它仅仅包含列表的只读操作所需要的成员:
public interface IReadOnlyList<out T>: IEnumerable<T>,IEnumerable{
int Count{get;}
T thislint index]{get;}
}
Array类
Array类是所有一维和多维数组的隐式基类,它是实现标准集合接口的最基本类型之一。Array类提供了类型统一性,所以所有的数组对象都能够访问它的一套公共的方法而与它们的声明或实际的元素类型无关。
**注:**Array本身是一个类,因此无论数组中的元素是什么类型,数组本身总是引用类型。
数组的复制
数组可以通过Clone方法进行复制,例如arrayB=arrayA.Clone()。但是,其结果是一个浅表副本(shallowclone),即表示数组本身的内存会被复制。如果数组中包含的是值类型的对象,那么这些值也会被复制;但如果包含的是引用类型的对象,那么只有引用会被复制(结果就是两个数组的元素都引用了相同的对象).
如果要深度复制必须遍历整个数组手动克隆每一个元素对象。
创建和索引
int[]myArray={1,2,3 };
int first =myArray [o];
int last=myArray[myArray.Length-1];
动态创建数组实例
//Create a string array 2 elements in length:
Array a=Array.CreateInstance(typeof(string),2);
a.SetValue("hi",0);
a.SetValue("there",1);
string s=(string)a.GetValue(o);
//We can also cast to a# array as follows:
string[]cSharpArray =(string[])a;
string s2= cSharpArray[o];
枚举
数组可以通过foreach语句进行枚举
int[] myArray={1,2,3};
foreach(int val in myArray)
Console.WriteLine(val);
静态方法Array.ForEach
int[] myArray = {1, 2, 3};
Array.ForEach(myArray, Console.WriteLine);
长度和维度
public int GetLengthpublic (int dimension)
public long GetLongLength (int dimension);
public int Length {get;}
public long LongLength{get;}
public int GetLowerBound(int dimension);
public int GetUpperBound(int dimension);
public int Rank{get;}// Returns number of dimensions in array
搜索
- BinarySearch方法:快速在排序数组中找到特定元素
- IndexOf /LastIndexOf方法:搜索未排序数组中的特定元素
- Find/FindLast/FindIndex/FindAll/Exists/TrueForA1l:搜索未排序数组中满足指定的Predicate的一个或多个元素。
排序
public static void Sort<T>(T[] array);
接受一对数组的排序方法将基于第一个数组的元素的排序结果对两个数组的元素进行相应的调整。在下面的例子中,数字和其对应的单词最终都将按数字顺序进行排列:
int[]numbers={3,2,1 };
string[]words ={"three","two","one" };
Array.Sort(numbers,words);
反转数组元素
public static void Reverse(Array array);
public static void Reverse(Array array, int index,int length);
复制数组
Array 提供了4个对数组进行浅表复制的方法:Clone、CopyTo、Copy和Constrainedcopy。
前两个是实例方法;后两个为静态方法。
- Clone方法返回一个全新的(浅表复制的)数组,CopyTo和Copy方法复制数组中的若干连续元素。若复制多维数组则需要将多维数组的索引映射为线性索引。例如,一个3x3数组的中间矩阵(position[1,1])的索引可以用4表示。其计算方法是1*3+1。这些方法允许源与目标范围重叠而不会造成任何问题。
- ConstrainedCopy执行一个原子操作:如果所有请求的元素无法成功复制(例如类型错误)那么操作将会回滚。
- Array还提供了一个AsReadOnly方法来包装数组以防止其中的元素被重新赋值。
转换和调整大小
Array.ConvertAll
:创建并返回一个包含指定元素类型TOutout的新数组。
float[] reals ={1.3f,1.5f,1.8f };
int[] wholes =Array.ConvertAll(reals,r=>Convert.ToInt32(r));
List、Queue、Stack、Set
List和ArrayList
泛型 List和非泛型ArrayList类都提供了一种可动态调整大小的对象数组实现。它们是集合类中使用最广泛的类型,ArrayList实现了IList而List既实现了IList又实现了IList。
List和ArrayList在内部都维护了一个对象数组,并在超出容量的时候替换为一个更大的数组。
如果T是一种值类型,那么List的速度会比ArrayList快好几倍因为List不需要对元素执行装箱和拆箱操作。
LinkedList
泛型的双向链表
创建链表
LinkedList<string> linkedList = new LinkedList<string>();
添加元素
linkedList.AddLast("Apple");
linkedList.AddLast("Banana");
linkedList.AddLast("Cherry");
遍历链表
foreach (var item in linkedList)
{
Console.WriteLine(item);
}
删除元素
linkedList.Remove("Banana");
linkedList.RemoveFirst();
linkedList.RemoveLast();
列表节点类
public sealed class LinkedListNode<T>{
public LinkedList<T> List{get; }
public LinkedListNode<T> Next { get;}
public LinkedListNode<T> Previous{get;}
public T Value{get;set;}
}
Queue和Queue
先进先出(FIFO),虽然队列是可枚举的,但是它并没有实现IList和IList,因为我们无法直接通过索引访问其成员。然而,可以使用ToArray方法将其中元素复制到一个数组中,而后进行随机访问:
常用方法
- Clear
- Contains
- CopyTo
- Count
- Dequeue 出
- Enqueue 入
- Peek
- ToArray
- TrimExcess
- GetEnumerator
Stack和Stack
Stack和Stack是后进先出(LIFO)
常用方法
- Stack(int capacity)
- Clear
- Contains
- CopyTo
- Count
- GetEnumerator
- Peek
- Pop
- Push
- ToArray
- TrimExcess
HashSet和SortedSet
- 它们的Contains方法均使用散列查找因而执行速度很快.
- 它们都不保存重复元素,并且都忽略添加重复值的请求。
- 无法根据位置访问元素
**注:**SortedSet按一定顺序保存元素,而Hashset则不是,HashSet是通过使用只存储键的散列表实现的;而Sortedset则是通过一个红/黑树实现的。
using System;
using System.Collections.Generic;
public class HashSetExample
{
public static void Main(string[] args)
{
// 创建一个HashSet
HashSet<string> set = new HashSet<string>();
// 添加元素
set.Add("Apple");
set.Add("Banana");
set.Add("Orange");
// 检查元素是否存在
bool hasApple = set.Contains("Apple");
Console.WriteLine("Has Apple: " + hasApple);
// 删除元素
set.Remove("Banana");
// 遍历元素
foreach (string item in set)
{
Console.WriteLine(item);
}
}
}
i
Add(T item)
: 添加元素到集合中,如果元素已存在,则不进行任何操作。Remove(T item)
: 从集合中删除指定元素。Contains(T item)
: 检查集合中是否包含指定元素。Count
: 返回集合中元素的数量。Clear()
: 清空集合中的所有元素。
using System;
using System.Collections.Generic;
public class SortedSetExample
{
public static void Main(string[] args)
{
// 创建一个SortedSet
SortedSet<string> set = new SortedSet<string>();
// 添加元素
set.Add("Apple");
set.Add("Banana");
set.Add("Orange");
// 检查元素是否存在
bool hasApple = set.Contains("Apple");
Console.WriteLine("Has Apple: " + hasApple);
// 删除元素
set.Remove("Banana");
// 遍历元素
foreach (string item in set)
{
Console.WriteLine(item);
}
}
}
Add(T item)
: 添加元素到集合中,如果元素已存在,则不进行任何操作。Remove(T item)
: 从集合中删除指定元素。Contains(T item)
: 检查集合中是否包含指定元素。Count
: 返回集合中元素的数量。Clear()
: 清空集合中的所有元素。Min
: 获取集合中的最小值。Max
: 获取集合中的最大值。
字典
字典是一种集合,其包含的元素均为键值对。字典通常用于查找或用作排序列表
Dictionary
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建字典
Dictionary<string, int> fruits = new Dictionary<string, int>();
// 添加元素
fruits.Add("apple", 1);
fruits["banana"]= 2;
fruits["orange"]= 3;
// 访问元素
Console.WriteLine("The value for 'apple' is: " + fruits["apple"]);
// 检查键是否存在
if (fruits.ContainsKey("banana"))
{
Console.WriteLine("The dictionary contains 'banana'.");
}
// 移除元素
fruits.Remove("orange");
// 遍历字典
foreach (KeyValuePair<string, int> kvp in fruits)
{
Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);
}
// 获取字典的大小
Console.WriteLine("The dictionary contains {0} elements.", fruits.Count);
// 清空字典
fruits.Clear();
Console.WriteLine("The dictionary contains {0} elements after clearing.", fruits.Count);
}
}
- C#中的
Dictionary<TKey, TValue>
是基于哈希表(Hash Table)实现的。哈希表是一种数据结构,它使用哈希函数将键映射到存储桶(bucket)或槽(slot),从而实现快速的查找、插入和删除操作。以下是字典的底层实现的一些关键点:1. 哈希函数
哈希函数用于将键转换为哈希码(hash code),哈希码是一个整数值,用于确定键在哈希表中的位置。C#中的每个对象都有一个
GetHashCode
方法,该方法返回对象的哈希码。字典使用这个哈希码来确定键的存储位置。2. 存储桶(Bucket)
哈希表由一个存储桶数组组成,每个存储桶可以存储一个或多个键值对。哈希码通过取模运算(
hashCode % bucketCount
)映射到存储桶的索引位置。3. 处理冲突
由于不同的键可能会映射到相同的存储桶(称为哈希冲突),字典需要一种机制来处理这些冲突。C#的
Dictionary
使用链地址法(Separate Chaining)来处理冲突,即每个存储桶实际上是一个链表,所有映射到同一存储桶的键值对都存储在这个链表中。4. 扩容
当字典中的元素数量达到一定阈值时,字典会自动扩容。扩容的过程包括创建一个更大的存储桶数组,并重新计算所有现有键的存储位置(重新哈希)。
在C#的
Dictionary<TKey, TValue>
中,扩容的阈值是由负载因子(load factor)和当前容量决定的。默认情况下,负载因子为0.75,这意味着当字典中的元素数量达到当前容量的75%时,字典就会进行扩容。5. 查找、插入和删除操作
- 查找:通过键的哈希码找到对应的存储桶,然后在该存储桶的链表中查找键值对。
- 插入:通过键的哈希码找到对应的存储桶,然后将键值对插入到该存储桶的链表中。如果键已经存在,则更新其对应的值。
- 删除:通过键的哈希码找到对应的存储桶,然后在该存储桶的链表中删除键值对。
OrderedDictionary
OrderedDictionary
与普通的Dictionary
不同,OrderedDictionary
保留了元素插入的顺序,这意味着你可以按插入顺序访问键值对。主要特点
- 有序性:
OrderedDictionary
保留了元素插入的顺序。- 键值对存储:与
Dictionary
类似,OrderedDictionary
也存储键值对。- 双向访问:可以通过键或索引访问元素。
ListDictionary和HybridDictionary
ListDictionary
ListDictionary
是一种简单的键值对集合,适用于包含少量元素的场景。它内部使用链表来存储元素,因此在元素数量较少时性能较好。特点
- 适用于小规模数据:由于使用链表存储,
ListDictionary
在元素数量较少时性能较好。- 线性查找:查找操作是线性的,随着元素数量的增加,查找时间也会增加。
- 有序性:保留元素插入的顺序。
HybridDictionary
HybridDictionary
是一种结合了ListDictionary
和Hashtable
优点的集合类。它在元素数量较少时使用ListDictionary
,当元素数量增加到一定程度时自动切换为Hashtable
,以提高性能。特点
- 动态调整:根据元素数量动态选择最优的数据结构。
- 性能优化:在元素数量较少时使用
ListDictionary
,在元素数量较多时使用Hashtable
。- 无序性:不保留元素插入的顺序
排序字典
排序字典(SortedDictionary)是 .NET 框架中的一个集合类,位于
System.Collections.Generic
命名空间中。与普通的Dictionary
不同,SortedDictionary
保证了键值对按照键的顺序进行排序。它内部使用二叉搜索树(通常是红黑树)来存储元素,因此能够在保持有序性的同时提供高效的查找、插入和删除操作。SortedDictionary既可以使用键也可以使用索引进行查找。主要特点
- 有序性:
SortedDictionary
保证键值对按照键的顺序进行排序。- 高效操作:查找、插入和删除操作的时间复杂度为 O(log n)。
- 键的唯一性:与
Dictionary
类似,SortedDictionary
中的键必须是唯一的。
*自定义集合
using System;
using System.Collections.ObjectModel;
public class PositiveIntegerCollection : Collection<int>
{
protected override void InsertItem(int index, int item)
{
if (item <= 0)
{
throw new ArgumentException("Only positive integers are allowed.");
}
base.InsertItem(index, item);
}
protected override void SetItem(int index, int item)
{
if (item <= 0)
{
throw new ArgumentException("Only positive integers are allowed.");
}
base.SetItem(index, item);
}
}
class Program
{
static void Main()
{
PositiveIntegerCollection collection = new PositiveIntegerCollection();
collection.Add(1);
collection.Add(2);
// collection.Add(-1); // 这行代码会抛出异常
foreach (int item in collection)
{
Console.WriteLine(item);
}
}
}
Linq
LINQ数据的基本组成部分是序列和元素。序列是任何实现了IEnumerable接口的对象,而其中的每一项称为一个元素。
string[]names ={ "Tom","Dick”,"Harry" };
IEnumerable<string>filteredNames =System.Ling,Enumerable.Where(names,n=>n.Length>=4);
foreach(string n in filteredNames )
Console.WriteLine(n);
精简代码
var filteredNames=names.Where(n=>n.Length>=4)
流式语法
查询运算符链
class LingDemo{
static void Main(){
string[]names ={ "Tom","Dick","Harry","Mary","Jay" };
IEnumerable<string>query=names
.Where(n=>n.Contains("a"))
.OrderBy(n =>n.Length)
.Select(n=>n.ToUpper());
foreach(string name in query) Console.WriteLine(name);//JAY MARY HARRY
}
}
使用Lambda表达式
Lambda表达式和Func的签名
标准运算符使用了泛型的Func委托。Func是System命名空间下的一系列通用泛型委托。它的定义满足以下要求:
Func中类型参数出现的先后次序和Lambda表达式中的参数顺序相同。
因此,Func<TSource,bool>所对应的Lambda表达式为TSource =>bool:接受一个 TSource 参数并返回一个 bool值。类似的,Func<TSource,TResult>所对应的Lambda表达式为TSource =>TResult
原始顺序
输入序列中的原始元素顺序对于LINQ来说非常重要。因为一些查询运算符,例如Take、Skip和Reverse,直接依赖这种顺序。
Take运算符将输出前x个元素,而丢弃其他元素,例如:
int[]numbers ={10,9,8,7,6}; IEnumerable<int>firstThree =numbers.Take(3); //{10,9,8 }
Skip运算符会跳过集合中的前x个元素而输出剩余的元素,例如:
IEnumerable<int> lastTwo = numbers.Skip(3);//{7,6 }
Reverse运算符会将集合中的所有元素反转,这和它的命名是一致的:
IEnumerable<int> reversed = numbers.Reverse();//{6,7,8,9,10}
其他运算符
LINQ中并非所有查询运算符都会返回序列。例如,针对元素的运算符可以从输入序列中返回单个元素,如First、Last、ElementAt运算符:
int[] numbers={10,9,8,7,6}; int firstNumber=numbers.First();//10 int lastNumber= numbers.Last();//6 int secondNumber =numbers.ElementAt(1);//9 int secondLowest =numbers.0rderBy(n=>n).Skip(1).First();//7
一些查询运算符接受两个输入序列。例如Concat运算符会将一个输入序列附加到另一个序列后面,而Union运算符除了附加之外还会去掉其中重复的元素:
int[]seq1 =1,2,3 }; int[]seq2 = {3,4,5}; IEnumerable<int>concat=seq1.Concat(seg2);//{1,2,3,3,4,5} IEnumerable<int>union=seq1.Union(seq2);//{1,2,3,4,5}
查询表达式
C#为LINQ查询提供了一种简化的语法结构,称为查询表达式。表面上,这种语法像是在C#中内嵌SQL,而实际上,其设计却来源于像LISP和Haskell 这样的函数式编程语言中的列表推导功能。
static void Main(){
string[]names ={ "Tom","Dick","Harry","Mary","Jay" };
IEnumerable<string>query=
from n in names
wheren.Contains("a")// Filter elements
orderby n.Length //Sort elements
select n.ToUpper(); //Translate each element(project)
foreach(string name in query) Console.WriteLine(name);
}
范围变量
紧跟在from 关键字后面的标识符称为范围变量。范围变量指向当前序列中即将进行操作的元素。
混合查询语法
如果没有合适的查询语法来支持查询运算符,那么我们可以混合使用上面介绍的两种查询方式。这种做法的唯一的限制是每一种查询语法组件必须都是完整的(例如,必须由from子句开始,以select或者group子结束)。
string[]names ={ "Tom","Dick","Harry","Mary","Jay" };
//以下表达式将计算包含字母“a”的字符串数目:
int matches =(from n in names where n.Contains("a")select n).Count();
延迟执行
大部分查询运算符的一个重要性质是它们并非在构造时执行,而是在枚举(即在枚举器上调用MoveNext)时执行.
var numbers=new List<int>{1};
IEnumerable<int>query=numbers.Select(n=>n*10);// Build query
numbers.Add(2);//Sneak in an extra element
foreach(int nin query)Console.Write(n+"|");// 10|20|
可见,在查询语句创建之后,向列表中新添加的数字也出现在了查询结果中。这是因为这些筛选和排序逻辑只会在foreach语句执行时才会生效,这称为延迟或懒惰执行。
重复执行
延迟执行的另一个后果是:当重复枚举时,延迟执行的查询也会重复执行:
var numbers=newList<int>(){1,2 };
IEnumerable<int>query=numbers.Select(n=>n*10);foreach(intnin query)Console.Write(n+"|");// 10|20
numbers.Clear();foreach(intnin query)Console.Write(n+"|");// <nothing>
劣势
无法缓存某一个时刻的查询结果
对于一些计算密集型查询(或依赖远程数据库的查询),重复执行会带来不必要的浪费
可以使用ToArray或ToList来避免重复执行
捕获变量
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int threshold = 3;
// 使用捕获变量 threshold
var filteredNumbers = numbers.Where(n => n > threshold).ToList();
// 修改捕获变量 threshold
threshold = 4;
// 重新执行查询
var filteredNumbersAfterChange = numbers.Where(n => n > threshold).ToList();
Console.WriteLine("Filtered numbers (threshold = 3):");
foreach (var number in filteredNumbers)
{
Console.WriteLine(number);
}
Console.WriteLine("Filtered numbers (threshold = 4):");
foreach (var number in filteredNumbersAfterChange)
{
Console.WriteLine(number);
}
}
}
Filtered numbers (threshold = 3):
4
5
Filtered numbers (threshold = 4):
5
**注:**LINQ查询通常是延迟执行的,这意味着查询在定义时不会立即执行,而是在枚举结果时才执行。因此,捕获变量的值在查询执行时可能已经改变。在循环中定义的捕获变量可能会导致意外的行为,因为所有的lambda表达式可能会引用同一个变量。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
// 捕获变量 i
actions.Add(() => Console.WriteLine(i));
}
// 执行所有的动作
foreach (var action in actions)
{
action();
}
}
}
**注:**此时action方法捕获的变量i始终是5,因为查询在枚举结果时才执行
*装饰器
查询语句的执行方式
子查询
子查询就是包含在另一个查询的Lambda表达式中的查询语句
string[]names ={ "Tom","Dick","Harry","Mary","Jay" };
IEnumerable<string>outerQuery=names
.Where(n => n.Length == names.OrderBy (n2 =>n2.Length)
.Select(n2 =>n2.Length).First());
//Tom,Jay
子查询与延迟执行
子查询中的元素相关运算符和聚合运算符,如First、Count,不会导致外部查询立即执行。延迟执行仍然会被外部查询引用。这是因为子查询是间接执行的:即在本地查询中,它通过委托驱动执行;而在解释型查询中,它通过表达式树来执行。
构造方式
渐进式查询构造
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// 第一步:定义基本查询
var query = numbers.Where(n => n > 2);
// 第二步:添加排序
query = query.OrderBy(n => n);
// 第三步:添加投影
var finalQuery = query.Select(n => n * 2);
// 查询在这里执行
foreach (var number in finalQuery)
{
Console.WriteLine(number); // 输出: 6, 8, 10
}
into关键字
into
关键字用于将查询的中间结果存储在一个新的范围变量中,这样可以在后续的查询中继续使用这些中间结果。into
关键字通常在group by
和join
子句中使用,以便对中间结果进行进一步的操作。
使用into分组
List<string> words = new List<string> { "apple", "banana", "apricot", "blueberry", "avocado" };
var query = from word in words
group word by word[0] into wordGroup
select new
{
FirstLetter = wordGroup.Key,
Words = wordGroup
};
foreach (var group in query)
{
Console.WriteLine($"Words that start with '{group.FirstLetter}':");
foreach (var word in group.Words)
{
Console.WriteLine(word);
}
}
使用into进行连接
List<int> numbers1 = new List<int> { 1, 2, 3 };
List<int> numbers2 = new List<int> { 2, 3, 4 };
var query = from n1 in numbers1
join n2 in numbers2 on n1 equals n2 into joinedNumbers
from jn in joinedNumbers.DefaultIfEmpty()
select new
{
Number1 = n1,
Number2 = jn
};
foreach (var result in query)
{
Console.WriteLine($"Number1: {result.Number1}, Number2: {result.Number2}");
}
使用into进行子查询
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = from n in numbers
where n > 2
select n into filteredNumbers
where filteredNumbers % 2 == 0
select filteredNumbers;
foreach (var number in query)
{
Console.WriteLine(number); // 输出: 4
}
查询的包装
使用方法包装
public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = GetEvenNumbersGreaterThanTwo(numbers);
foreach (var number in result)
{
Console.WriteLine(number); // 输出: 4
}
}
public static IEnumerable<int> GetEvenNumbersGreaterThanTwo(IEnumerable<int> numbers)
{
return from n in numbers
where n > 2 && n % 2 == 0
select n;
}
}
使用扩展方法
public static class QueryExtensions
{
public static IEnumerable<int> GetEvenNumbersGreaterThanTwo(this IEnumerable<int> numbers)
{
return from n in numbers
where n > 2 && n % 2 == 0
select n;
}
}
public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = numbers.GetEvenNumbersGreaterThanTwo();
foreach (var number in result)
{
Console.WriteLine(number); // 输出: 4
}
}
}
简单包装
IEnumerable<string>query=
from n1 in
( from n2 in names
select n2.Replace("a","").Replace("e","")
)
where n1.Length >2 orderby n1 select n1;
映射方式
目前为止,select子句都直接映射为标量元素类型,为了映射更复杂的类型,可以使用C#的对象初始化器
匿名类型对象
public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = from n in numbers
where n > 2
select new
{
OriginalNumber = n,
SquaredNumber = n * n
};
foreach (var item in query)
{
Console.WriteLine($"Original: {item.OriginalNumber}, Squared: {item.SquaredNumber}");
}
}
}
let关键字
string[] names ={"Tom","Dick","Harry","Mary","Jay" };
IEnumerable<string>query=
from n in names let vowelless=
n.Replace("a","").Replace("e","").
Replace("i","").Replace("o","").Replace("u","")
where vowelless.Length>2
orderby vowelless
.select n; // Thanks to let,n is still in scope
AsEnumerable方法
AsEnumerable
是 LINQ 中的一个扩展方法,它用于将某个集合转换为IEnumerable<T>
类型。这个方法通常用于在查询链中将某个集合的类型从特定的集合类型(如List<T>
或IQueryable<T>
)转换为通用的IEnumerable<T>
,以便在后续的操作中应用 LINQ 的标准查询运算符。
Linq运算符
LINQ(Language Integrated Query)运算符是用于查询和操作数据的标准方法集合。它们可以应用于各种数据源,包括集合、数据库、XML 等。LINQ 运算符分为以下几类:
1. 过滤运算符(Filtering Operators)
- Where:筛选序列中的元素。
var query = numbers.Where(n => n > 2);
2. 投影运算符(Projection Operators)
- Select:将序列中的每个元素投影到一个新形式。
var query = numbers.Select(n => n * 2);
- SelectMany:将每个元素投影到一个集合,并将结果集合合并为一个序列。
var query = people.SelectMany(p => p.Phones);
3. 排序运算符(Sorting Operators)
- OrderBy:按升序排序元素。
var query = numbers.OrderBy(n => n);
- OrderByDescending:按降序排序元素。
var query = numbers.OrderByDescending(n => n);
- ThenBy:在先前排序结果的基础上进行次级排序(升序)。
var query = people.OrderBy(p => p.LastName).ThenBy(p => p.FirstName);
- ThenByDescending:在先前排序结果的基础上进行次级排序(降序)。
var query = people.OrderBy(p => p.LastName).ThenByDescending(p => p.FirstName);
4. 连接运算符(Join Operators)
- Join:根据匹配条件连接两个序列。
var query = customers.Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.CustomerName, o.OrderID });
- GroupJoin:根据匹配条件连接两个序列,并将结果分组。
var query = customers.GroupJoin(orders, c => c.CustomerID, o => o.CustomerID, (c, os) => new { c.CustomerName, Orders = os });
5. 分组运算符(Grouping Operators)
- GroupBy:将序列中的元素分组。
var query = numbers.GroupBy(n => n % 2 == 0 ? "Even" : "Odd");
6. 合运算符(Set Operators)
- Distinct:移除序列中的重复元素。
var query = numbers.Distinct();
- Union:返回两个序列的并集。
var query = numbers1.Union(numbers2);
- Intersect:返回两个序列的交集。
var query = numbers1.Intersect(numbers2);
- Except:返回第一个序列中不在第二个序列中的元素。
var query = numbers1.Except(numbers2);
7. 元素运算符(Element Operators)
- First:返回序列中的第一个元素。
var first = numbers.First();
- FirstOrDefault:返回序列中的第一个元素,如果序列为空则返回默认值。
var firstOrDefault = numbers.FirstOrDefault();
- Last:返回序列中的最后一个元素。
var last = numbers.Last();
- LastOrDefault:返回序列中的最后一个元素,如果序列为空则返回默认值。
var lastOrDefault = numbers.LastOrDefault();
- Single:返回序列中的唯一元素,如果序列不包含恰好一个元素则抛出异常。
var single = numbers.Single();
- SingleOrDefault:返回序列中的唯一元素,如果序列为空则返回默认值,如果序列包含多个元素则抛出异常。
var singleOrDefault = numbers.SingleOrDefault();
8. 聚合运算符(Aggregate Operators)
- Count:返回序列中的元素个数。
var count = numbers.Count();
- Sum:返回序列中数值元素的总和。
var sum = numbers.Sum();
- Average:返回序列中数值元素的平均值。
var average = numbers.Average();
- Min:返回序列中的最小值。
var min = numbers.Min();
- Max:返回序列中的最大值。
var max = numbers.Max();
9. 转换运算符(Conversion Operators)
- ToArray:将序列转换为数组。
var array = numbers.ToArray();
- ToList:将序列转换为列表。
var list = numbers.ToList();
- ToDictionary:将序列转换为字典。
var dictionary = people.ToDictionary(p => p.LastName);
10. 生成运算符(Generation Operators)
- Range:生成一个指定范围内的整数序列。
var range = Enumerable.Range(1, 10);
- Repeat:生成包含一个重复值的序列。
var repeat = Enumerable.Repeat("Hello", 5);
11. 量词运算符(Quantifier Operators)
- Any:确定序列中的任何元素是否满足条件。
var hasAny = numbers.Any(n => n > 2);
- All:确定序列中的所有元素是否都满足条件。
var all = numbers.All(n => n > 0);
12. 分区运算符(Partitioning Operators)
- Take:返回序列中的指定数量的元素。
var topThree = numbers.Take(3);
- Skip:跳过序列中的指定数量的元素,然后返回剩余的元素。
var skipTwo = numbers.Skip(2);
- TakeWhile:返回序列中的元素,只要它们满足指定的条件。
var takeWhile = numbers.TakeWhile(n => n < 4);
- SkipWhile:跳过序列中的元素,只要它们满足指定的条件,然后返回剩余的元素。
var skipWhile = numbers.SkipWhile(n => n < 3);
对象销毁与垃圾回收
有些对象需要显式依靠销毁代码来释放资源。例如,打开的文件、锁、操作系统句柄和非托管对象。它们在.NET的术语中称为销毁(disposal),相应的功能则由IDisposable接口提供。此外,那些占用了托管内存但不再使用的对象必须在某个时间回收。这个功能称为垃圾回收,它由 CLR执行。
IDisposable接口、Dispose方法和Close方法
public interface IDisposable{
void Dispose();
}
C#的 using语句从语法上提供了调用实现 IDisposable接口对象的 Dispose 方法的捷径。它会将相应的语句包裹在try/finally语句块中。
finally语句块保证了Dispose方法即使在抛出异常,或者语句块执行提前结束的情况下也一定会被调用。
简单的情况下,编写自定义的可笑会类型只需要实现IDisposable接口并编写Dispose方法即可
sealed class Demo :IDisposable{
public void Dispose(){
//Perform cleanup /tear-down.
...
}
}
标准销毁语义
- 1.对象一旦销毁就无法再恢复,也不能够重新激活。在销毁之后继续调用其方法(除Dispose之外)或访问其属性都将抛出0bjectDisposedException。
- 2.可以重复调用对象的Dispose方法,且不会发生任何错误
- 3.若可销毁对象 x“拥有”可销毁对象y,则x的Dispose 方法会自动调用y的 Dispose方法,接到其他指令的情况除外。
在销毁时清理字段
Dispose方法本身并没有释放内存,只有垃圾回收时才会释放内存
自动垃圾回收
垃圾回收并非在不被引用之后立即执行,而是周期性的。
GC会更频繁地回收最新的代,而旧的代(存活时间长的对象)则不会频繁的进行回收。
根
根可以使对象保持存活。如果对象没有直接或者间接地被根引用,那么它就可以被垃圾回收器回收了。
根有以下几种:
- 当前正在执行的方法(或在其调用栈的任何一个方法中)的局部变量或者参数
- 静态变量
- 终结队列中的对象
终结器
若对象拥有终结器,则在对象从内存中释放之前,会执行终结器.
终结器和构造器的声明方式很像。但它以~字符作为前缀.
class Test{
~Test(){
...
}
}
有终结器的对象不会被直接删除会放在一个特殊的队列中,当终结器执行完才会进行删除,此时这个特殊队列扮演着根对象的角色。并且在终结器执行完毕之后对象会成为未引用对象,并在下一次垃圾回收时删除。
缺点:
- 终结器会降低内存分配和回收的速度(GC需要对终结器的执行进行追踪)
- 终结器延长了对象和该对象所引用的对象的生命周期(它们必须等到下一次垃圾回收时才会被真正删除)。
- 无法预测多个对象的终结器调用的顺序。
- 开发者对于终结器调用的时机只有非常有限的控制能力。如果一个终结器的代码阻塞,则其他对象也无法终结
- 如果应用程序没有被完全卸载,则对象的终结器也可能无法得以执行。
垃圾回收器的工作方式
GC会从根对象开始按照对象引用遍历对象图,将所有遍历到的对象标记为可达对象,没有被标记的会被直接删除(若对象没有终结器的话)
优化技术
分代回收
垃圾回收器将堆上的内存分为了三代。刚刚分配的对象位于第0代;在第一轮回收中存活的对象在第1代,而其他所有对象为第2代。第0代和第1代对象就是所谓的短生存期(ephemeral)的代。
大对象堆
大对象堆(Large Object Heap,LOH)是 .NET 框架中的一个专门用于存储大对象的内存区域。大对象通常是指那些占用内存超过 85,000 字节的对象,如大型数组、字符串等。LOH 的设计目的是优化内存管理和垃圾回收(GC)性能,因为大对象的分配和回收开销较大。
并发与异步
线程
要创建并启动一个线程,需要首先实例化Thread对象并调用Start方法。Thread的最简单的构造器接收一个Threadstart委托:一个无参数的方法,表示执行的起始位置。例如:
Thread t = new Thread(WriteY);
t.Start();
for(int i=0;i<1000;i++) Console.Write("X");
static void WriteY(){
for(int i=0;i<1000;i++) Console.Write("Y");
}
图:线程
注:线程是抢占式的。
汇合与休眠
调用Thread
的Join
方法可以等待线程结束.
Thread t=new Thread(Go);
t.Start();
t.Join();
Console.WriteLine("thread t has ended!");
static void Go(){for(inti=0;i<1000;i++)console.Write("y");}
Thread.Sleep
Thread.Sleep(TimeSpan.FromHours(1));//休眠一小时
Thread.Sleep(500);//休眠500ms
阻塞
当线程由于特定原因暂停执行那么他就是阻塞的。可以使用ThreadState属性测试现成的阻塞状态
Threadstate是一个标志枚举类型。它由“三层二进制位组成。然而,其中的大多数值都是冗余、无用或者废弃的。以下的扩展方法将Threadstate限定为以下四个有用的值之一:Unstarted、RunningWaitSleepJoin、Stopped:
I/O密集和计算密集
如果一个操作的绝大部分时间都在等待事件的发生,则称为IO密集,例如下载网页或者调用Console.ReadLine。(I0密集操作一般都会涉及输入或者输出,但是这并非硬性要求。例如Thread.sleep也是一种I/O密集的操作)。而相反的,如果操作的大部分时间都用于执行大量的CPU操作,则称为计算密集。
阻塞与自旋
阻塞(Blocking)
阻塞是指线程在等待某个条件或资源时,主动放弃CPU的执行权,进入等待状态。当条件满足或资源可用时,线程被唤醒并继续执行。阻塞通常通过操作系统提供的同步原语(如互斥锁、信号量、条件变量等)来实现。
优点:
- 节省CPU资源:阻塞线程不占用CPU时间,适用于长时间等待的场景。
- 简单易用:大多数编程语言和操作系统都提供了丰富的阻塞同步原语。
缺点:
- 上下文切换开销:阻塞和唤醒线程需要操作系统进行上下文切换,可能导致性能开销。
自旋(Spinning)
自旋是指线程在等待某个条件或资源时,不放弃CPU的执行权,而是不断地检查条件是否满足。这种机制通常通过忙等待(busy-waiting)来实现,即在循环中反复检查条件。
优点:
- 低延迟:适用于短时间等待的场景,因为没有上下文切换开销。
- 简单实现:自旋锁的实现通常较为简单。
缺点:
- 浪费CPU资源:自旋线程会占用CPU时间,可能导致资源浪费,尤其是在等待时间较长的情况下。
- 不适用于长时间等待:长时间自旋会严重影响系统性能。
本地状态与共享状态
本地状态
using System;
using System.Threading;
class Program
{
// 定义线程本地变量
private static ThreadLocal<int> _threadLocal = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId);
static void Main()
{
Thread t1 = new Thread(PrintThreadLocal);
Thread t2 = new Thread(PrintThreadLocal);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintThreadLocal()
{
Console.WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}, Thread Local Value: {_threadLocal.Value}");
}
}
共享状态
using System;
using System.Threading;
class Program
{
// 定义共享变量
private static int _sharedValue = 0;
// 定义锁对象
private static readonly object _lock = new object();
static void Main()
{
Thread t1 = new Thread(IncrementSharedValue);
Thread t2 = new Thread(IncrementSharedValue);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Final Shared Value: {_sharedValue}");
}
static void IncrementSharedValue()
{
for (int i = 0; i < 1000; i++)
{
lock (_lock)
{
_sharedValue++;
}
}
}
}
* 锁与线程安全
向线程传递数据
lambda表达式
static void Main(){
Thread t=newThread(()=>Print("Hello from t!"));
t.Start();
}
static void Print(string message){ Console.WriteLine(message);}
线程的优先级
enum ThreadPriority{fLowest,BelowNormal, Normal, AboveNormal, Highest}
信号发送
有时一个线程需要等待来自其他线程的通知,即所谓的信号发送(signaling)。最简单的信号发送结构是ManualResetEvent。调用ManualResetEvent的WaitOne方法可以阻塞当前线程,直到其他线程调用了Set“打开”了信号。
using System;
using System.Threading;
class Program
{
private static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
static void Main()
{
Thread t1 = new Thread(WaitForSignal);
t1.Start();
Console.WriteLine("Main thread doing some work...");
Thread.Sleep(2000); // Simulate work
Console.WriteLine("Main thread sending signal.");
manualResetEvent.Set(); // Send signal
t1.Join();
}
static void WaitForSignal()
{
Console.WriteLine("Thread waiting for signal...");
manualResetEvent.WaitOne(); // Wait for signal
Console.WriteLine("Thread received signal.");
}
}
线程池
每当启动一个线程时,都需要一定的时间(几百毫秒)来创建新的局部变量栈。而线程池通过预先创建一个可回收线程的池子来降低这个开销。线程池对开发高性能的并行程序和细粒度的并发都是非常必要的。它可以支持运行一些短暂的操作而不会受到线程启动开销的影响。
- 线程池中线程的Name属性是无法进行设置的,因此会增加代码调试的难度(但可以在调试时使用 Visual Studio的Threads窗口附加一个描述信息)。
- 线程池中的线程都是后台线程
- 阻塞线程池中的线程将影响性能(请参见14.2.13.2)。
进入线程池
在线程池上运行代码的最简单的方式是调用Task.Run。
Console.WriteLine("hello Main");
Task task = Task.Run(() => Console.WriteLine("hello Thread pool"));
await task;
任务
启动任务
Task默认使用线程池中的线程,它们都是后台线程。这意味着当主线程结束时,所有的任务也会随之停止。因此,要在控制台应用程序中运行这些例子,必须在启动任务之后阻塞主线程(例如在任务对象上调用wait,或者调用Console.ReadLine()方法).
Console.WriteLine("hello Main");
Task.Run(() => Console.WriteLine("hello Thread pool"));
Console.ReadLine();
Task.Run 会返回一个Task对象,它可以用于监控任务的执行过程。这一点与Thread对象不同(注意,我们没有在Task.Run之后调用Start,这是因为Task.Run 创建的任务是“热”的任务;)
我们可以使用Task的Status 属性来追踪其执行状态。
Wait方法
调用Task的Wait方法可以阻塞当前方法,直到任务完成,这和调用线程对象的Join方法相似
Console.WriteLine("hello Main");
Task task = Task.Run(() => Console.WriteLine("hello Thread pool"));
task.Wait();
长任务
在线程池上运行一个长时间执行的任务并不会造成问题,但是如果要并行运行多个长时间运行的任务(特别是会造成阻塞的任务),则会对性能造成影响。在这种情况下,相比于使用TaskCreationOptions.LongRunning而言,更好的方案是:
- 如果运行的是I/0密集型任务,则使用TaskCompletionsource和异步函数(asynchronousfunctions)通过回调函数而非使用线程实现并发性。
- 如果任务是计算密集型,则使用生产者/消费者队列可以控制这些任务造成的并发数量,避免出现线程和进程饥饿的问题(参见23.7节)。
返回值
Task有一个泛型子类Task,它允许任务返回一个值。如果在调用Task.Run时传入一个Func委托(或者兼容的Lambda表达式)替代Action 就可以获得一个Task对象:
此后,通过查询Result属性就可以获得任务的返回值。如果当前任务还没有执行完毕,则调用该属性会阻塞当前线程,直至任务结束。
Console.WriteLine("hello Main");
Task<int> task = Task.Run(() =>
{
Console.WriteLine("hello Thread pool");
return 1;
});
Console.WriteLine(task.Result);
异常
任务可以方便地传播异常,这和线程是截然不同的。因此,如果任务中的代码抛出一个未处理异常(换言之,如果你的任务出错(fault)),那么调用Wait()或者访问Task的Result属性时,该异常就会被重新抛出:
TaskCompletionSource类
TaskCompletionSource可以创建一个任务,但是这种任务并非那种需要执行启动操作并在随后停止的任务;而是在操作结束或出错时手动创建的“附属”任务。这非常适用于IO 密集型的工作。它不但可以利用任务所有的优点(能够传递返回值、异常或延续)而且不需要在操作执行期间阻塞线程。
Task.Delay方法
Task.Delay
是一个异步方法,用于创建一个在指定时间段后完成的任务。它不阻塞当前线程,而是返回一个可以等待的任务。
Thread.Sleep
是一个同步方法,用于暂停当前线程的执行一段时间。它会阻塞调用它的线程,直到指定的时间过去。
static async void Task1()
{
Console.WriteLine("开始异步延迟...");
await Task.Delay(2000); // 延迟2秒
Console.WriteLine("延迟结束。");
}
Task1();
Console.ReadLine();
异步原则
同步操作与异步操作
同步操作(synchronousoperation)先完成其工作再返回调用者.
异步操作(asynchronousoperation)的大部分工作则是在返回给调用者之后才完成的.
什么是异步编程
异步编程的原则是以异步的方式编写运行时间很长(或者可能很长)的函数。这和编写长时间运行的函数的传统同步方法正好相反。它会在一个新的线程或者任务上调用这些函数,从而实现需要的并发性.
C#的异步函数
await
async
等待
var result =await expression;
statement(s);
等同于:
var awaiter=expression.GetAwaiter();
awaiter.OnCompleted(()=>
{
var result=awaiter.GetResult();
statement(s);
});
添加了async修饰符的方法称为异步函数,这是因为通常它们本身也是异步的。为了解释这一点我们需要了解异步函数的执行过程。
当遇到await 表达式时,通常情况下执行过程会返回到调用者上。就像是迭代器中的yield return一样。但是,运行时在返回之前会在等待的任务上附加一个延续,保证任务结束时,执行点会跳回到方法中,并继续执行剩余的代码。
编写异步函数
async Task Print()
{
Console.WriteLine("开始异步延迟...");
await Task.Delay(2000); // 延迟2秒
Console.WriteLine("延迟结束。");
}
await Print();
返回Task
async Task<int> GetAnswerToLife()
{
await Task.Delay(5000);
int answer=21*2;//Method has return type Task<int> we return int
return answer;
}
异步Lambda表达式
async Task NamedMethod(){
awaiter Task.Delay(1000);
Console.WriteLine("Foo");
}
Func<Task> unnamed = async ()=>{
awaiter Task.Delay(1000);
Console.WriteLine("Foo");
}
流与I/O
.NET流的架构主要包含三个概念:后台存储、装饰器以及流适配器
要使用后台存储,则必须公开相应的接口。而Stream正是实现这个功能的.NET标准类。它支持标准的读、写以及定位方法。它与数组不同,流并不会直接将数据存储在内存中,流会以每次一个字节或者每次一块数据的方式按照序列处理数据。因此,无论后台存储大小如何,流都只会占用很少的内存。
流可以分为两类:
后台存储流:它们是与特定的后台存储类型连接的流,例如FileStream或者NetworkStream。
装饰器流:这些流会使用其他的流,并以某种方式转换数据。例如Deflatestream或者CryptoStream。
总之,后台存储流负责处理原始数据;装饰器流可以透明地进行二进制数据的转换(例如加密);而适配器则提供了处理更高级类型(例如文本和XML)的方法。
使用流
抽象的stream类是所有流的基类。它的方法和属性定义了三种基本的操作:读、写查找。除此之外,它还定义了一些管理性的任务,例如关闭、刷新(fush)和配置超时时间.
读取和写入
string filePath = "example.txt";
string textToWrite = "Hello, FileStream!";
// 写入文件
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
byte[] data = System.Text.Encoding.UTF8.GetBytes(textToWrite);
fs.Write(data, 0, data.Length);
}
// 读取文件
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] data = new byte[fs.Length];
fs.Read(data, 0, data.Length);
string readText = System.Text.Encoding.UTF8.GetString(data);
Console.WriteLine($"Read from FileStream: {readText}");
}
查找
如果Canseek返回true,那么表示当前的流是可以查找的。在一个可以查找的流中(例如文件流),不但可以查询还可以修改它的长度Length(调用SetLength方法)。也可以通过Position属性随时设置读写的位置(Position属性的位置是相对于流的起始位置的)。而Seek方法则可以参照当前位置或者结束位置进行位置的设置。
如果流不支持查找功能(例如加密流),那么确定其长度的唯一方法就是遍历整个流。而且,如果需要重新读取先前的位置,则必须关闭整个流,然后重新从头开始读取。
关闭和刷新
流在使用结束后必须销毁,以释放底层资源,例如文件和套接字句柄。可以在using语句块中创建流的实例来确保结束后销毁流对象。通常,流对象的标准销毁语义为:
- Dispose和Close方法的功能是一样的。
- 重复销毁或者关闭流对象不会产生任何错误
关闭一个装饰器流会同时关闭装饰器及其后台存储流。关闭装饰器链的最外层装饰器(链条的头部)就可以关闭链条中的所有对象。
有一些流(例如文件流)会将数据缓冲到后台存储中并从中取回数据,减少回程,从而提高性能。这意味着,写入流的数据并不会直接存储到后台存储中,而是会先将缓冲区填满再写入存储器。Flush方法可以强制将缓冲区中的数据写入后台存储中。当流关闭的时候,也会自动调用Flush方法。因此以下代码是没有必要的:s.Flush();s.Close();
超时
如果流的CanTimeout属性返回true那么可以为这个流对象设置读写超时时间,ReadTimeout和WriteTimeout
线程安全
使用Synchronized返回一个线程安全的包装器,包装器使用排他锁保证线程安全。
FileStream类
创建FileStream
- File的静态方法
FileStream fs = File.OpenRead(filePath);//只读打开文件
FileStream fs = File.OpenWrite(filePath);//只写
FileStream fs = File.Create(filePath);//读/写。
File的快捷方法
1. File.WriteAllText
将字符串写入文件。如果文件不存在,将创建文件;如果文件存在,将覆盖文件内容。
2. File.ReadAllText
从文件中读取所有文本。
3. File.WriteAllBytes
将字节数组写入文件。如果文件不存在,将创建文件;如果文件存在,将覆盖文件内容。
4. File.ReadAllBytes
从文件中读取所有字节。
5. File.AppendAllText
将字符串追加到文件末尾。如果文件不存在,将创建文件。
6. File.Exists
检查文件是否存在。
7. File.Delete
删除指定文件。
8. File.Copy
复制文件到新位置。如果目标文件存在,可以选择覆盖它。
9. File.Move
将文件移动到新位置。
10. File.ReadLines
逐行读取文件内容,返回
IEnumerable<string>
。
- 实例化FileStream
FileStream(String, FileMode, FileAccess, FileShare, Int32, FileOptions)
var fs =new FileStream(filePath,FileMode.Open);
FileMode mode:指定如何打开或创建文件的枚举值。主要选项包括:
FileMode.Create
:创建新文件。如果文件已存在,将其覆盖。FileMode.CreateNew
:创建新文件。如果文件已存在,则抛出异常。FileMode.Open
:打开现有文件。如果文件不存在,则抛出异常。FileMode.OpenOrCreate
:打开文件(如果存在);否则,创建新文件。FileMode.Append
:打开文件并定位到文件尾。如果文件不存在,则创建新文件。FileMode.Truncate
:打开现有文件并清空其内容。FileAccess access:指定对文件的访问权限。主要选项包括:
FileAccess.Read
:只读访问。FileAccess.Write
:只写访问。FileAccess.ReadWrite
:读写访问。FileShare share:指定其他进程如何访问文件。主要选项包括:
FileShare.None
:不共享,禁止其他进程访问。FileShare.Read
:允许其他进程读取文件。FileShare.Write
:允许其他进程写入文件。FileShare.ReadWrite
:允许其他进程读写文件。Int32 bufferSize:缓冲区的大小,以字节为单位。默认值为4096字节。
Boolean useAsync:指示是否启用异步I/O操作。
true
表示启用异步I/O。FileOptions options:提供更多高级文件选项的枚举值。主要选项包括:
FileOptions.None
:没有其他选项。FileOptions.Asynchronous
:异步文件操作。FileOptions.DeleteOnClose
:文件在关闭时删除。FileOptions.SequentialScan
:文件按顺序访问。FileOptions.RandomAccess
:文件随机访问。
MemoryStream类
MemoryStream使用数组作为后台存储。这在一定程度上与使用流的目的是相违背的,因为这个后台存储都必须一次性地驻留在内存中。然而,Memorystream仍然有一定的用途。例如,随机访问一个不可查找的流。如果原始的流的大小可以承受,则可以通过如下的方式将其复制到MemoryStream中:
var ms =new MemoryStream(); sourceStream.CopyTo(ms);
MemoryStream的关闭和刷新不是必须的。如果关闭了一个MemoryStream就无法再次读写了。但是我们仍然可以调用ToArray方法来获得底层的数据。而刷新操作则不会对内存流执行任何操作。
PipeStream
管道有两种类型:
- 匿名管道(速度更快):支持在同一个计算机中的父进程和子进程之间进行单向通信。
AnonymousPipeServerStream
和AnonymousPipeClientStream
- 命名管道(更加灵活):允许同一台计算机的任意两个进程之间,或者不同计算机(使用 Windows 网络)的两个进程间进行双向通信。
NamedPipeServerStream
和NamedPipeClientStream
命名管道
命名管道可以让通信各方使用名称相同的管道进行通信。其协议定义了两种不同的角色:客户端与服务器。客户端和服务器之间的通信采用以下方式:
- 服务器实例化一个NamedPipeServerStream,然后调用WaitForConnection方法。
- 客户端实例化一个NamedPipeClientstream,然后调用Connect(可提供可选的超时时间)。
匿名管道
匿名管道支持在父子进程之间进行单向通信。匿名管道不会使用系统范围内的名称,而是通过一个私有句柄进行调整。
- 服务器实例化一个AnonymousPipeServerstream对象,并提交一个值为In或者0ut的PipeDirection。
- 服务器调用GetClientHandleAsstring方法获得一个管道的标识符,然后传递回客户端(一般作为启动子进程的一个参数)。
- 子进程实例化一个AnonymousPipeClientStream对象,指定相反的Pipe-Direction。
- 服务器调用DisposeLocalcopyOfclientHandle方法释放第2步中生成的本地句柄。
- 父子进程通过读/写流进行通信。
BufferedStream
Bufferedstream可以装饰或者包装另外一个具有缓冲功能的流.
using (FileStream fs = new FileStream("./Static/test.txt", FileMode.Open))
using (BufferedStream bs = new BufferedStream(fs, 20000))
{
Console.WriteLine(bs.ReadByte());
Console.WriteLine(fs.Position);//20000
Console.WriteLine(bs.Position);//1
}
上述示例会提前将数据读取到缓冲区,因此虽然仅仅读取了一个字节,但是底层流已经向前读取了20000字节。因此剩余的19999次ReadByte调用就不需要再次访问Filestream了。
流适配器
- 文本适配器:TextReader,TextWriter,StreamReader,StreamWriter,StringReader,StringWriter
- 二进制适配器:BinaryReader, BinaryWriter
- xml适配器:XmlReader,XmlWriter
文本适配器
TextReader和TextWriter都是专门处理字符和字符串的适配器的抽象基类。它们在框架中各有两个通用的实现:
- StreamReader/StreamWriter:使用Stream存储其原始数据,将流的字节转
换为字符或者字符串。- StringReader/StringWriter:使用内存字符串实现了TextReader/TextWriter。
using (FileStream fs = new FileStream("./Static/test.txt", FileMode.Open))
using (TextWriter writer = new StreamWriter(fs))
{
writer.WriteLine("陈志明");
writer.WriteLine("1234");
}
using (FileStream fs = new FileStream("./Static/test.txt", FileMode.Open))
using (TextReader reader = new StreamReader(fs))
{
Console.WriteLine(reader.ReadLine());
Console.WriteLine(reader.ReadLine());
}
文本适配器通常和文件有关,因此File类也为此提供了一些静态方法,诸如CreateText、AppendText以及OpenText:
string filePath = "example.txt";
string content = "Hello, File.CreateText!";
using (TextWriter writer = File.CreateText(filePath))
{
writer.WriteLine(content);
}
using (TextWriter writer = File.AppendText(filePath))
{
writer.WriteLine(content);
}
using (TextReader reader = File.OpenText(filePath))
{
while (reader.Peek()>-1)
{
Console.WriteLine(reader.ReadLine);
}
}
StringReader和StringWriter
StringReader和StringWriter适配器并不包装流;相反,它们使用一个字符串或者StringBuilder作为底层数据源。这意味着它们不需要进行任何的字节转换。事实上,这些类所执行的操作都可以通过字符串或者StringBuilder与一个索引变量轻松实现。上述类型和StreamReader/StreamWriter共享相同的基类,这也是它们的优势所在。
二进制适配器
BinaryReader和BinaryWriter能够读写基本的数据类型:bool、byte、chardecimal、float、double、short、int、sbyte、ushort、uint、ulong以及string和基元类型的数组。
public class Person{
public string Name;
public int Age ;
public double Height;
}
public void SaveData(stream s){
var w=new BinaryWriter(s);
w.Write(Name);
w.Write(Age);
w.Write(Height);
w.Flush();
}
// Ensure the BinaryWriter buffer is cleared.
//We won't dispose/close it,so more data
// can be written to the stream.
public void LoadData(Stream s){
var r=new BinaryReader(s);
Name =r.ReadString();
Age=r.ReadInt32();
Height=r.ReadDouble();
}
BinaryReader也可以将数据读入字节数组。以下代码将读取一个可查找流中的全部内容
byte[]data=new BinaryReader(s).ReadBytes((int)s.Length);
关闭和销毁流适配器
销毁流适配器的方式有四种:
- 1.只关闭适配器
- 2.关闭适配器,而后关闭流
- 3.(对于写入器)先刷新适配器,而后关闭流
- 4.(对于读取器)直接关闭流
*压缩流
System.I0.Compression命名空间中提供了两个通用的压缩流:Deflatestream和GZipstream。这两个类都使用了与ZIP格式类似的常见的压缩算法。其区别是,GZipstream会在开头和结尾处写入额外的协议信息,其中包括检测错误的CRC。
操作压缩文件
可以使用System.I0.Compression命名空间(位于System.I0.Compression.FileSystem.dll中)中的ZipArchive 和ZipFile 来操作常用的ZIP压缩格式的文件。与Deflatestream和GZipStream相比,这种格式的优点是可以处理多个文件,并可以兼容Windows资源管理器及其他压缩工具创建的ZIP文件。
ZipArchive可以操作流,而ZipFile则执行更加常见的文件操作
ZipFile中的CreateFromDirectory方法可以将指定目录的所有文件添加到一个ZIP文中:
ZipFile.CreateFromDirectory(@"d:\MyFolder",@"d:\compressed.zip");//压缩 ZipFile.ExtractToDirectory(@"d:\compressed.zip", @"d:\MyFolder");//解压
ZipFile的Open方法可用于读/写各个文件项目,这个方法会返回一个ZipArchive对象(也可以从Stream对象创建ZipArchive实例)。调用Open时必须指定一个文件名,并指定存档的操作方式:Read、Create或者Update。然后就可以枚举Entries属性遍历现有项目了。还可以调用GetEntry方法来查询某一个具体的文件:
using(ZipArchive zip=ZipFile.0pen(@"d:\zz.zip",ZipArchiveMode.Read))
foreach(ZipArchiveEntry entryin zip.Entries)
Console.WriteLine(entry.FullName +""+ entry.Length);
文件与目录操作
静态类:File和Directory
实例方法类(使用文件或者目录名创建):FileInfo和DirectoryInfo此外,还有一个特殊的静态类Path。它不操作文件或目录,但是它可以处理文件名称或者目录路径字符串。同时Path还可以用于临时文件的处理.
File类
File是一个静态类,它的方法均接受文件名参数。这个参数可以是相对于当前目录的路径也可以是一个完整的路径。以下是它的一些方法(所有的public和static方法):
**注:**Move方法会在目标文件存在的情况下抛出一个异常,但Replace方法则不会,这两个方法都可以重命名文件,或将文件移动到另一个目录下。
文件安全性
GetAccessControl和SetAccessControl方法支持通过(System.Security.AccessControl命名空间下的)FileSecurity对象查询或修改操作系统授予用户和角色的权限。在创建一个新文件时,我们还可以给Filestream的构造器传递一个Filesecurity对象来指定该文件的权限。
Directory类
静态Directory类和File类似
- CreateDirectory
CreateDirectory(string path)
:创建指定路径的目录,包括所有必需的子目录。- Delete
Delete(string path)
:删除指定路径的目录。默认情况下,此方法将删除空目录,但可以使用重载来指定是否递归删除非空目录。- Exists
Exists(string path)
:检查指定路径的目录是否存在。- GetDirectories
GetDirectories(string path)
:返回指定目录中的所有子目录的路径。- GetFiles
GetFiles(string path)
:返回指定目录中的所有文件的路径。- Move
Move(string sourceDirName, string destDirName)
:将目录从一个路径移动到另一个路径。- EnumerateDirectories
EnumerateDirectories(string path)
:以枚举方式返回指定目录中的所有子目录的路径。- EnumerateFiles
EnumerateFiles(string path)
:以枚举方式返回指定目录中的所有文件的路径。
FileInfo类和DirectoryInfo类
FileInfo类以实例成员的形式提供了File 类型静态方法的大部分功能。此外还包含一些额外的属性,如Extensions、Length、IsReadOnly以及Directory(返回-个DirectoryInfo对象)
DirectoryInfo枚举文件和子目录:
DirectoryInfo di=new DirectoryInfo(@"e:\photos");
foreach(FileInfo fi in di.GetFiles("*.jpg"))
Console.WriteLine(fi.Name);
foreach(DirectoryInfo subDir in di.GetDirectories())
Console.WriteLine(subDir.FullName);
Path类型
- Combine:
Combine(string path1, string path2)
:将两个路径片段合并为一个完整的路径。这个方法会根据需要自动添加路径分隔符。- GetDirectoryName:
GetDirectoryName(string path)
:返回指定路径字符串的目录信息,不包括最后一个目录分隔符。- GetFileName:
GetFileName(string path)
:返回指定路径字符串的文件名和扩展名部分。- GetExtension:
GetExtension(string path)
:返回指定路径字符串的文件扩展名(包括点号.
)。- GetFileNameWithoutExtension:
GetFileNameWithoutExtension(string path)
:返回指定路径字符串的文件名部分,不包括扩展名。- GetFullPath:
GetFullPath(string path)
:返回指定路径字符串的绝对路径。如果路径是相对路径,将基于当前工作目录获取绝对路径。- IsPathRooted:
IsPathRooted(string path)
:确定指定的路径字符串是否包含根目录或驱动器信息。- ChangeExtension:
ChangeExtension(string path, string extension)
:更改指定路径字符串的文件扩展名。- GetTempPath:
GetTempPath()
:返回操作系统的临时文件夹路径。- HasExtension:
HasExtension(string path)
:确定指定的路径字符串是否具有文件扩展名。- GetInvalidPathChars 和 GetInvalidFileNameChars:
- 分别返回无效路径字符和无效文件名字符的字符数组。
特殊文件夹
Path和Directory类型并不具备查找特殊文件夹的功能。这些特殊文件夹包括MyDocument、Program Files、Application Data等。该功能是由System.Environment类的GetFolderPath方法提供的。
查询卷信息
我们可以使用DriveInfo类来查询计算机驱动器相关的信息:
捕获文件系统事件
FileSystemWatcher类可以监控一个目录(或者子目录)的活动。不论哪一个用户或者进程在该目录下创建、修改、重命名、删除文件或子目录,或者更改其属性时,FileSystem Watch类的事件都会触发.
*网络
System.Net.*命名空间中包含了支持各种网络标准的类,支持的标准包括HTTP、TCP/IP 以及FTP等。以下列出了其中的主要组件:
- WebClient类:支持通过HTTP或者FTP执行简单的下载/上传操作。
- WebRequest和WebResponse类:可以从底层控制客户端HTTP或FTP操作。
- HttpClient类:消费HTTP Web API和RESTfu1服务。
- HttpListener类:用于编写HTTP服务器
- SmtpClient类:构造并通过SMTP协议发送邮件。
- Dns类:用于进行域名和地址之间的转换。
- TcpClient、UdpClient、TcpListener和Socket类:用于直接访问传输层和网络层
.NET网络架构
网络术语:
地址与端口
**地址:**IPV4,IPV6
端口:
TCP和UDP协议将每一个IP地址划分为65535个端口,从而允许一台计算机在一个地址上运行多个应用程序,每一个应用程序使用一个端口。许多应用程序都分配有标准端口,例如,HTTP默认使用80端口,而SMTP使用25端日。
using System.Net;
IPAddress iPAddress = IPAddress.Parse("192.168.1.1");
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, 8848);
Console.WriteLine("ip:" + iPEndPoint.ToString());
客户端类型
WebRequest和WebResponse
WebClient是一个易于使用的门面(facade)类,它负责调用WebRequest和Web-Response的功能,从而节省很多的代码。
HttpClient是另一个基于WebRequest和WebResponse的类
WebClient类
以下列出了 Webclient 类型的使用步骤:
- 1.实例化一个 Webclient 对象。
- 2.设置Proxy属性值。
- 3.若需要进行验证,则设置Credentials 属性值。
- 4.使用相应的 URI调用DownloadXXX或者UploadXXX方法。
以下方法会尝试下载一个Web页面,并进行进度报告。如果下载时间大于5秒,则取消下载任务:
var wc =new WebClient();
Wc.DownloadProgressChanged+=(sender,args)=>
Console.WriteLine(args.ProgressPercentage+"%complete");
Task.Delay(5000).ContinueWith(ant=>wc.CancelAsync());
await wc.DownloadFileTaskAsync("http://oreilly.com","webpage.htm");
WebRequest和WebRespose
····················
注:网络学习起来有点枯燥与无用,所以下面省略许多用法,具体知识点请参考书籍(若有需要的话)
序列化
序列化的概念
序列化是将内存中的对象或者对象图(一组相互引用的对象)拉平为一个可以保存或进行传输的字节流,或者XML节点。反序列化正好相反,它把数据流重新构造成内存中的一个对象或者对象图。
序列化和反序列化通常用于:
- 通过网络或程序边界传输对象
- 在文件或者数据库中保存对象
序列化引擎
在.NETFramework中有4种序列化机制:
- 数据契约序列化器
- 二进制序列化器((用于桌面应用程序)
- (基于特性的)XML序列化器(XmlSerializer)
- IXmlSerializable 接口
数据契约的序列化
要使用数据契约序列化器,需要以下几步:
- 1.决定是选用 DataContractSerializer还是NetDataContractSerializer。
- 2.使用[DataContract]和[DataMember】特性修饰要序列化的类型和成员。
- 3.实例化序列化器后调用Write0bject和Readobject。
如果选择 DataContractSerializer,则需要同时注册已知类型(即能够序列化的子类型),并且要决定是否保留对象引用。
此外,还需要采取特殊的措施确保集合能够被正确序列化。
数据契约序列化器有两种:
- DataContractSerializer:.NET类型与数据契约类型松耦合。
- NetDataContractSerializer:.NET类型与数据契约类型紧耦合。
使用序列化器
选择序列化器之后,下一步需要在要序列化的类型和成员上添加相应的特性标注。至少应当添加以下这些标注:
在每个类型上添加
[DataContract]
特性在每一个需要序列化的成员上添加
[DataMember]
特性
using System.Runtime.Serialization;
[DataContract]
public class MyClass
{
[DataMember(Name = "id")]
public int Id { get; set; }
[DataMember(Name = "name")]
public string Name { get; set; }
}
实例化DataContractSerializer
using System.IO;
using System.Runtime.Serialization;
Person p = new Person { Name = "Stacey", Age = 30 };
var ds = new DataContractSerializer(typeof(Person));
using (Stream s = File.Create("person.xml"))
ds.WriteObject(s, p);
// Serialize
Person p2;
using (Stream s = File.OpenRead("person.xml"))
p2 = (Person)ds.ReadObject(s);
Console.WriteLine(p2.Name + "" + p2.Age);
或者XmlSerializer
using System.IO;
using System.Runtime.Serialization;
using System.Xml.Serialization;
Person p = new Person { Name = "Stacey", Age = 30 };
var serializer = new XmlSerializer(typeof(Person));
using (FileStream fs = new FileStream("person.xml", FileMode.Create))
{
serializer.Serialize(fs, p);
}
// Deserialize
Person p2;
using (FileStream fs = new FileStream("person.xml", FileMode.Open))
{
p2 = (Person)serializer.Deserialize(fs);
}
// Output deserialized data
Console.WriteLine(p2.Name + " " + p2.Age); // Ensure proper spacing between Name and Age
public record Person
{
public string Name { get; init; } // 使用 init 访问器来实现不可变性
public int Age { get; init; }
}
序列化子类
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
// 基类
[DataContract]
[KnownType(typeof(Employee))]
[KnownType(typeof(Student))]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
}
// 派生类1
[DataContract]
public class Employee : Person
{
[DataMember]
public int EmployeeId { get; set; }
[DataMember]
public decimal Salary { get; set; }
}
// 派生类2
[DataContract]
public class Student : Person
{
[DataMember]
public string SchoolName { get; set; }
[DataMember]
public int GradeLevel { get; set; }
}
class Program
{
static void Main()
{
// 创建一个包含多个子类的列表
List<Person> people = new List<Person>
{
new Employee { Name = "John Doe", Age = 35, EmployeeId = 1001, Salary = 50000 },
new Student { Name = "Jane Smith", Age = 20, SchoolName = "ABC School", GradeLevel = 10 }
};
// 使用DataContractSerializer进行序列化
DataContractSerializer serializer = new DataContractSerializer(typeof(List<Person>),
new Type[] { typeof(Employee), typeof(Student) });
using (FileStream fs = new FileStream("people.xml", FileMode.Create))
{
serializer.WriteObject(fs, people);
}
// 反序列化
List<Person> people2;
using (FileStream fs = new FileStream("people.xml", FileMode.Open))
{
people2 = (List<Person>)serializer.ReadObject(fs);
}
// 输出反序列化后的对象属性
foreach (var person in people2)
{
if (person is Employee emp)
{
Console.WriteLine($"Employee: Name={emp.Name}, Age={emp.Age}, EmployeeId={emp.EmployeeId}, Salary={emp.Salary}");
}
else if (person is Student student)
{
Console.WriteLine($"Student: Name={student.Name}, Age={student.Age}, School={student.SchoolName}, Grade={student.GradeLevel}");
}
}
}
}
对象引用
契约中引用的对象也会被序列化。
[DataContract]public class Person{
[DataMember]public string Name;
[DataMember] public int Age;
[DataMember]public Address HomeAddress;
}
[DataContractlpublic class Address{
[DataMember]public string street, Postcode;
}
保留对象引用
NetDataContractSerializer总是会保留引用的相等性,而DataContractSe-
rializer除非专门指定,否则不会保留。这意味着若同一个对象在两个不同的地方引用,则DataContractSerializer将会把它写入两次。如果我们更改先前的示例,给Person添加一个WorkAddress:
成员顺序
数据契约序列化器对数据成员的顺序要求极为苛刻。反序列化器会跳过任何判定为序列之外的成员。
在序列化时,成员会按照如下的顺序进行书写:
- 1.从基类型到子类型
- 2.从低Order值到高Order值(对于使用了Order特性修饰的数据成员)
- 3.字母表顺序,使用字符串的序数(ordinal)进行排序。
[DataContract]public class Person{ [DataMember (0rder=0)] public string Name; [DataMember (0rder=1)] public int Age; }
null和空值
处理nu11和值为空的数据成员的方式有两种
1.显式写入null或者空值(默认方式)2.序列化输出时忽略这些数据成员
[DataContract]public class Person{
[DataMember(EmitDefaultValue=false)public string Name;
[DataMember(EmitDefaultValue=false)public int Age;
}
数据契约与集合
数据契约序列化器可以保存并恢复任何可枚举的集合
[DataContract]public class Person{
[DataMember]public List<Address> Addresses;
}
[DataContract]public class Address{
[DataMember] public string street, Postcode;
}
子类集合元素
序列化器处理子类集合元素的过程是透明的。当需要使用子类时,必须声明有效的子类类型:
[DataContract,KnownType(typeof(UsAddress))]
public class Address{
[DataMember]public string street, Postcode;
}
public class UsAddress :Address {}
*二进制序列化器
*xml序列化
反射和元数据
反射和激活类型
获取类型
System.Type的实例代表了类型的元数据。由于Type的应用领域非常广泛,因此它存在于System命名空间,而非System.Reflection命名空间中。
调用任意对象的GetType方法或者使用C#的typeof运算符都可以得到System.Type的实例。
Type type = typeof(string);
string name = type.Name;
// Type baseType = type.BaseType;
Assembly assembly = type.Assembly;
bool isPublic = type.IsPublic;
获取数组类型
Type simpleArrayType=typeof(int).MakeArrayType();
Console.WriteLine(simpleArrayType ==typeof(int[]));// True
MakeArrayType方法可以接受一个int类型的参数以创建多维矩形数组:
Type cubeType=typeof(int).MakeArrayType(3);// cube shaped
Console.WriteLine(cubeType ==typeof(int[,,]));// True
GetElementType方法可以返回数组元素的类型:
Type e=typeof (int[]).GetElementType();
//e==typeof(int)
而GetArrayRank则可以返回矩形数组的维数:
int rank =typeof(int[,,]).GetArrayRank();
//3
获取嵌套类型
要获得嵌套类型,可以在包含类型上调用GetNestedTypes方法。例如:
foreach(Type t in typeof(System.Environment).GetNestedTypes())
Console.WriteLine(t.FullName);
类型名称
类型具有Namespace、Name以及FullName属性。在大多数情况下,FullName 是前两者的组合:
Type t = typeof(StringBuilder);
Console.WriteLine(t.FullName);
Console.WriteLine(t.Name);
Console.WriteLine(t.Namespace);
嵌套类型名称
对于嵌套类型来说,其FullName仅仅是包含的类型名称:
Type t = typeof(System.Environment.SpecialFolder);
Console.WriteLine(t.FullName);
Console.WriteLine(t.Name);
Console.WriteLine(t.Namespace);
泛型类型名称
泛型类型名称带有’后缀,后续加上类型参数的数目。如果泛型类型是未绑定的类型则其Name和FullName都将遵循该规则:
Type t = typeof(Dictionary<,>);
Console.WriteLine(t.Name);
Console.WriteLine(t.FullName);
数组和指针类型名称
数组类型的名称和typeof表达式使用的后缀是相同的:
Console.WriteLine(typeof int[]).Name);// Int32[]
Console.WriteLine(typeof(int[,]).Name);// Int32[,]
Console.WriteLine(typeof(int,]).FullName);// System.Int32[,]
基本类型和接口
Type base1 = typeof (System.string).BaseType;
Type base2=typeof(System.I0.FileStream).BaseType;
Console.WriteLine(base1.Name);// object
Console.WriteLine(base2.Name);// Stream
实例化类型
从类型创建对象实例的方式有两种:
- 调用静态的Activator.CreateInstance方法;
- 调用ConstructorInfo.Invoke方法,并使用Type的GetConstructor方法的返回值作为参数(高级的对象实例化场景)。
泛型类型
Type既可以表示封闭的泛型类型也可以表示未绑定类型参数的泛型类型。在编译时只能够实例化封闭的泛型类型,而无法实例化未绑定的泛型类型:
Type closed=typeof(List<int>);
List<int>list =(List<int>)Activator.CreateInstance(closed);
Type unbound=typeof(List<>);
object anError =Activator.CreateInstance(unbound);// Runtime error
MakeGenericType方法接受类型参数,即可将未绑定的泛型类型转换为封闭的泛型类型:
Type closed =unbound.MakeGenericType(typeof(int));
Type unbound=typeof(List<>);
反射并调用成员
GetMembers方法可以返回类型的成员。
MemberInfo[] members = typeof(Walnut).GetMembers();
foreach(MemberInfo m in members)
Console.WriteLine(m);
class Walnut
{
private bool cracked; public void Crack() { cracked = true; }
}
如果在调用GetMembers方法时不传递任何参数,则该方法会返回当前类型(及其基类)的所有公有成员。GetMember方法则可以通过名称检索特定的成员。由于成员可能会被重载,因此该方法仍然会返回一个数组:
MemberInfo[] m=typeof(Walnut).GetMember("Crack");
Console.WriteLine(m[0]);
class Walnut
{
private bool cracked; public void Crack() { cracked = true; }
}
MemberInfo对象拥有Name属性以及两种Type属性:
DeclaringType:该属性返回该成员的定义类型。ReflectedType:返回调用GetMembers的具体类型
MethodInfo test=typeof(Program).GetMethod("Tostring");
MethodInfo obj= typeof (object).GetMethod("ToString");
Console.WriteLine(test.DeclaringType);// System.0bject
Console.WriteLine(obj.DeclaringType);//System.0bject
Console.WriteLine(test.ReflectedType);// Program
Console.WriteLine(obj.ReflectedType);//System.0bject
成员类型
MemberInfo本身相比于具体成员是非常轻量的,这是因为它是图19-1中所有类型的抽象基类:
每一个MemberInfo的子类都有大量的属性和方法,包含了该成员各个方面的元数据。
其中包含可见性、修饰符、泛型参数列表、参数、返回类型和自定义属性。
MethodInfo m=typeof(Walnut).GetMethod("Crack");
Console.WriteLine(m);// Void Crack()
Console.WriteLine(m.ReturnType);// System.Void
泛型类型成员
我们不但可以从未绑定的泛型类型中获得成员元数据,也可以从封闭的泛型类型中获得这些数据:
PropertyInfo unbound =typeof(IEnumerator<>).GetProperty("Current")
PropertyInfo closed=typeof(IEnumerator<int>).GetProperty("Current");
Console.WriteLine(unbound);//T Current
Console.WriteLine(closed);// Int32 Current
动态调用成员
一旦得到了MethodInfo、PropertyInfo或者FieldInfo对象,我们就可以动态对其进行调用或者得到并设置它们的值。这称为“动态绑定”或者“后期绑定”。它会在运行时(而不是在编译时)来决定成员的调用。
string s="Hello";
int length=s.Length;
//通过反射动态实现相同效果
object s="Hello";
PropertyInfo prop=s.GetType().GetProperty("Length");
int length=(int)prop.GetValue(s,null);
Console.WriteLine(length);
GetValue和SetValue可以获得/设置PropertyInfo或者FieldInfo的值。第一个参数是类型的实例。若调用静态成员,则该参数为nu11。访问索引器和访问属性类似,但是当调用GetValue或者SetValue 时需要提供索引器的值作为第二个参数。
调用MethodInfo的Invoke方法,并提供一个数组作为方法的参数表,即可动态调用方法。如果传递的参数类型错误,则运行时会抛出异常。动态调用舍弃了编译时的类型安全性,但是和dynamic关键字一样,它仍然能够保证运行时的类型安全性。
// 创建 MyClass 的实例
MyClass myObject = new MyClass();
// 获取 SayHello 方法的 MethodInfo
MethodInfo methodInfo = typeof(MyClass).GetMethod("SayHello");
// 准备调用方法的参数数组
object[] parameters = new object[] { "Alice" };
// 调用方法
methodInfo.Invoke(myObject, parameters);
public class MyClass
{
public void SayHello(string name)
{
Console.WriteLine($"Hello, {name}!");
}
}
方法的参数
动态方式调用Substring
//正常
Console.WriteLine("stamp".Substring(2));
//反射
Type type=typeof(string);
Type[]parameterTypes ={typeof(int)};
MethodInfo method = type.GetMethod("Substring", parameterTypes);
object[] arguments={2};
object returnValue =method.Invoke("stamp", arguments);
Console.WriteLine(returnValue);
MethodBase(MethodInfo和ConstructorInfo的基类)的GetParameters方法将会返回方法参数的元信息。以下示例延续了上一个例子,获得方法的参数:
ParameterInfo[] parameterInfos= methodInfo.GetParameters();
foreach (ParameterInfo parameterInfo in parameterInfos)
{ Console.WriteLine(parameterInfo.Name);
Console.WriteLine(parameterInfo.ParameterType);
}
处理ref和out参数
若需要传递ref和out 参数,可以在获得方法之前调用相应类型的MakeByRefType
int x;
bool successfulParse=int.TryParse("23",out x);
//使用反射
object[] args1 = { "23", 0 };
Type[] argTypes = { typeof(string),typeof(int).MakeByRefType()};
MethodInfo method = typeof(int).GetMethod("TryParse",argTypes);
bool successfulParse =(bool)method.Invoke(null, args1);
Console.WriteLine(successfulParse);
使用委托提高性能
动态调用的效率不高,其开销通常为几微秒。如果要在一个循环中重复调用某个方法则可以为目标动态方法动态实例化一个委托,这样就可以将微秒级的开销降低到纳秒级。在以下示例中,我们将动态调用string的Trim方法一百万次而不会出现显著的开销:
//使用自定义委托
MethodInfo method=typeof(string).GetMethod("Trim",new Type[0]);
var trim=(Stringtostring) Delegate.CreateDelegate(typeof(Stringtostring),method);
for (int i = 0; i < 1000; i++)
{
Console.WriteLine( trim("test"));
}
delegate string Stringtostring(string s);
//使用Func委托
MethodInfo method=typeof(string).GetMethod("Trim",new Type[0]);
var trim=(Func<string,string>) Delegate.CreateDelegate(typeof(Func<string, string>),method);
for (int i = 0; i < 1000; i++)
{
Console.WriteLine( trim("test"));
}
*访问非公有成员
泛型方法
泛型方法无法直接调用需要调用MakeGenericMethod
:
public static T Echo<T>(T x) { return x; }
MethodInfo echo = typeof(Program).GetMethod("Echo");
MethodInfo intEcho = echo.MakeGenericMethod(typeof(int));
Console.WriteLine(intEcho.IsGenericMethodDefinition);
Console.WriteLine(echo.Invoke(null,new object[] {3}));
反射程序集
若需要动态反射程序集,只需调用Assembly对象的GetType或者GetTypes即可.
Type t =Assembly.GetExecutingAssembly().GetType("Demos.TestProgram");
以下示例列出了 e:\demo目录下程序集 mylib.dll 中的所有类型:
Assembly a= Assembly.LoadFrom(@"e:\demo\mylib.dll");
foreach(Type t in a.GetTypes())
Console.WriteLine(t);
特性
在C#中,特性(Attributes)是一种用于在声明的代码元素(如类、方法、属性等)上添加元数据的机制。特性允许开发者为代码元素附加额外的信息,这些信息可以在运行时通过反射进行访问。特性通常用于提供关于代码行为、结构或其它元数据的附加信息,以便在程序运行时能够动态地进行检查、修改或者选择性地执行某些操作。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class MyCustomAttribute : Attribute
{
// 可以在这里定义特性的属性
public string Description { get; set; }
// 可以在构造函数中初始化特性的属性
public MyCustomAttribute(string description)
{
Description = description;
}
}
//AttributeUsage 属性指定了特性可以应用于的目标,例如类或方法。AllowMultiple = true 表示允许在同一个目标上多次使用该特性。
使用特性
[MyCustom("This is a class attribute")]
class MyClass
{
[MyCustom("This is a method attribute")]
public void MyMethod()
{
// Method logic here
}
}
访问特性
MyClass myObj = new MyClass();
Type type = myObj.GetType();
// 获取类级别的特性
var classAttributes = type.GetCustomAttributes(typeof(MyCustomAttribute), false);
foreach (MyCustomAttribute attr in classAttributes)
{
Console.WriteLine("Class attribute description: " + attr.Description);
}
// 获取方法级别的特性
var methodInfo = type.GetMethod("MyMethod");
var methodAttributes = methodInfo.GetCustomAttributes(typeof(MyCustomAttribute), false);
foreach (MyCustomAttribute attr in methodAttributes)
{
Console.WriteLine("Method attribute description: " + attr.Description);
}
高级线程处理
同步概述
同步(synchronization)是指协调并发操作,得到可以预测的结果的行为。
同步结构可以分为三类:
- 排它锁:排它锁每一次只允许一个线程执行特定的活动或一段代码。它的主要目的是令线程访问共享的写状态而不互相影响。排它锁包括lock、Mutex和SpinLock。
- 非排它锁:非排它锁实现了有限的并发性。非排它锁包括Semaphore(slim)和ReaderWriterLock(Slim)
- 信号发送结构:这种结构允许线程在接到一个或者多个其他线程的通知之前保持阻塞状态。信号发送结构包括ManualResetEvent(Slim)、AutoResetEventCountdownEvent和Barrier。前三者就是所谓的事件等待柄(event wait handles)
排他锁
排它锁结构有三种:lock语句、Mutex和SpinLock。其中lock是最方便,最常用的结构。而其他两种结构多用于处理特定的情形。
- Mutex可以跨越多个进程(计算机范围锁)。
- SpinLock可用于实现微优化。它可以在高并发场景下减少上下文切换
lock语句
下面代码会导致竞态问题。
int counter = 0; // 在顶级语句中声明变量
Task.Run(() => IncrementCounter());
Task.Run(() => IncrementCounter());
Console.ReadLine();
void IncrementCounter()
{
for (int i = 0; i < 1000000; i++)
{
// 非原子操作,多个线程同时访问可能导致竞态条件
counter++;
}
Console.WriteLine($"Current counter value: {counter}");
}
使用lock后
using System;
using System.Threading.Tasks;
int counter = 0; // 在顶级语句中声明变量
object lockObject = new object();
Task.Run(() => IncrementCounter());
Task.Run(() => IncrementCounter());
Console.ReadLine();
void IncrementCounter()
{
{
for (int i = 0; i < 100000; i++)
{
lock (lockObject)
{
counter++;
}
}
}
Console.WriteLine($"Current counter value: {counter}");
}
选择同步对象
若一个对象在各个参与线程中都是可见的,那么该对象就可以作为同步对象。但是该对象必须是一个引用类型的对象(这是必须满足的条件)。同步对象通常是私有的(因为这样便于封装锁逻辑),而且一般是实例字段或者静态字段。同步对象本身也可以是被保护的对象,如以下示例中的 list字段:
class ThreadSafe{
List <string> _list =new List<string>();
void Test(){
lock(_list){
_list.Add("item 1");
...
}
}
}
如果一个字段仅作为锁存在(如前一节中的 locker),则可以精确地控制锁的范围和粒度。而容器的对象(this)乃至对象的类型也可以用作同步对象:
lock(this){...}
或者lock(typeof(Widget)){…}
使用锁的时机
使用锁的基本原则是:若需要访问可写的共享字段,则需要在其周围加锁。
下面展示不安全与安全代码
class ThreadUnsafe
{
private static int _x;
static void Increment()
{
_x++;
}
static void Assign()
{
_x = 123;
}
}
class ThreadUnsafe
{
private static int _x;
private static readonly object _locker = new object();
static void Increment()
{
lock (_locker)
{
_x++;
}
}
static void Assign()
{
lock (_locker)
{
_x = 123;
}
}
}
如果不使用锁:
- 诸如变量自增这类操作并不是原子操作,甚至变量的读写,在某些情况下也不是原子操作。
- 为了提高性能,编译器、CLR乃至处理器都会调整指令的执行顺序并在CPU的寄存器中缓存变量值。只要这种优化不会影响单线程程序的(或者使用锁的多线程程序的)行为即可。
使用信号发送结构同样可以完成锁的功能,保证了线程安全。
var signal=new ManualResetEvent(false);
int x=0;
new Thread(()=>{x++;signal.Set();}).Start();
signal.WaitOne();
Console.WriteLine(x);
锁与原子性
如果使用同一个锁对一组变量的读写操作进行保护,那么可以认为这些变量的读写操作是原子的(atomically)。假设我们只在locker锁中对x和y字段进行读写:
lock(locker){if (x!=0) y/x;}
x和y是以原子方式访问
嵌套锁
线程可以用嵌套的方式重复锁住同一个对象;
lock(locker){
lock(locker)
lock(locker){
...
}
}
注:只有最外层的lock语句退出时对象的锁才会解除。
当一个方法调用另一个方法的时候嵌套锁很有用
static readonly object locker=new object();
static void Main(){
lock(locker){
AnotherMethod();//We still have the lock-because locks are reentrant.
}
}
static void AnotherMethod(){
lock(locker){
Console.WriteLine("Another method");
}
}
//线程只会阻塞在第一个锁上
死锁
两个线程互相等待对方占用的资源就会使双方都无法继续执行,从而形成死锁.
object locker1 =new object();
object locker2=newobject();
new Thread(()=>{
lock(locker1)
Thread.sleep(1000);
lock(locker2);
}).start();
lock(locker2){
Thread.Sleep(1000);
lock(locker1);
}
Mutex
Mutex和C#的lock类似,但是它可以支持多个进程。换言之,Mutex不但可以用于应用程序范围,还可以用于计算机范围。在非竞争的情况下获得或者释放Mutex需要大约一微秒的时间,大概比lock要慢20倍。
Mutex类的WaitOne方法将获得该锁,ReleaseMutex方法将释放该锁。Mutex只能在获得锁的线程释放锁。
如果直接调用Mutex的Close或Dispose方法,但不调用ReleaseMutex则所有等待该Mutex的线程都将抛出AbandonedMutexException异常
非排他锁
信号量
信号量(semaphore)就像俱乐部一样:它有特定的容量,还有门卫保护。一旦满员之后,就不允许其他人进入了,人们只能在外面排队。每当有人离开时,才准许另外一个人进入。信号量的构造器需要至少两个参数:即俱乐部当前的空闲容量,以及俱乐部的总容量。
容量为1的信号量和Mutex和lock类似,但是信号量没有持有者这个概念,它是线程无关的。任何线程都可以调用Semaphore的Release方法。Mutex和lock则不然,只有持有锁的线程才能够释放锁。
class TheClub{
static SemaphoreSlim _sem =new SemaphoreSlim(3);// Capacity of 3
static void Main(){
for(int i=1;i<=5;i++)
new Thread (Enter).Start(i);
}
static void Enter(object id){
Console.WriteLine(id+"wants to enter");
_sem.Wait();
Console.WriteLine(id+"is in!");
Thread.Sleep(1000*(int)id);
Console.WriteLine(id+"is leaving");
_sem.Release();
}
}
读写锁
ReaderWriterLockSlim和ReaderWriterLock都拥有两种基本的锁,即读锁和写锁:
- 写锁是全局排它锁
- 读锁可以兼容其他的读锁
ReaderWriterLockSlim比ReaderWriterLock快上数倍。
因此,一个持有写锁的线程将阻塞其他任何试图获取读锁或写锁的线程(反之亦然)。但
是如果没有任何线程持有写锁的话,那么其他任意数量的线程都可以并发获得读锁。
using System;
using System.Threading;
class Program
{
static ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
static int resource = 0;
static void Main()
{
Thread writer1 = new Thread(WriteOperation);
Thread reader1 = new Thread(ReadOperation);
Thread reader2 = new Thread(ReadOperation);
writer1.Start();
reader1.Start();
reader2.Start();
writer1.Join();
reader1.Join();
reader2.Join();
Console.WriteLine("All threads completed.");
}
static void ReadOperation()
{
rwl.EnterReadLock();
Console.WriteLine("Reading resource value: " + resource);
Thread.Sleep(1000); // Simulate reading
rwl.ExitReadLock();
}
static void WriteOperation()
{
rwl.EnterWriteLock();
Console.WriteLine("Writing to resource");
resource++;
Thread.Sleep(1000); // Simulate writing
rwl.ExitWriteLock();
}
}
可升级锁
可升级锁和读锁还有一个重要的区别:虽然可升级锁可以和任意数目的读锁并存,但是一次只能获取一个可升级锁。这可以将锁的升级竞争序列化从而避免在升级中出现死锁,这和SQLServer中的更新锁是一致的。
EnterUpgradeableReadLock()
using System;
using System.Threading;
class Program
{
static ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
static int resource = 0;
static void Main()
{
Thread writerThread = new Thread(WriteOperation);
Thread readerThread1 = new Thread(ReadOperation);
Thread readerThread2 = new Thread(UpgradeableReadOperation);
writerThread.Start();
readerThread1.Start();
readerThread2.Start();
writerThread.Join();
readerThread1.Join();
readerThread2.Join();
Console.WriteLine("All threads completed.");
}
static void ReadOperation()
{
rwl.EnterReadLock();
Console.WriteLine("Reading resource value: " + resource);
Thread.Sleep(1000); // Simulate reading
rwl.ExitReadLock();
}
static void UpgradeableReadOperation()
{
rwl.EnterUpgradeableReadLock();
Console.WriteLine("Upgradeable read: " + resource);
// Example of upgrading to write lock conditionally
if (resource == 0)
{
rwl.EnterWriteLock();
resource = 1; // Simulate an update
rwl.ExitWriteLock();
}
rwl.ExitUpgradeableReadLock();
}
static void WriteOperation()
{
rwl.EnterWriteLock();
Console.WriteLine("Writing to resource");
resource++;
Thread.Sleep(1000); // Simulate writing
rwl.ExitWriteLock();
}
}
*递归锁
使用时间等待句柄发送信号
最简单的信号发送结构是事件等待句柄(eventwaithandles)。注意它和C#的事件是无关的。事件等待句柄有三种实现:AutoResetEvent、ManualResetEvent(Slim)和CountdownEvent。前两种基于通用的EventWaitHandle类,它们继承了基类的所有功能。
AutoResetEvent
AutoResetEvent就像验票机的闸门一样:插入一张票据只允许一个人通过。其名称中的 Auto 指的是开放的闸机在行人通过后会自动关闭或重置。线程可以调用WaitOne方法在闸机门口等待、阻塞(在“一个”闸机前等待,直至闸机门开启)。调用Set方法即向闸机中插入一张票据。如果有一系列的线程调用了WaitOne,那么它们会在闸机后排队等待注2。票据可以来自任何线程,即任何一个能够访问AutoResetEvent对象的非阻塞线程都可以调用Set方法来释放一个阻塞的线程
创建AutoResetEvent的方法有两种。第一种是使用其构造器
var auto =new AutoResetEvent(false);
(如果在构造器中以true为参数,则相当于立刻调用Set方法)。
第二种方法则是使用如下方式创建AutoResetEvent:
var auto =new EventWaitHandle(false,EventResetMode.AutoReset);
class BasicWaitHandle{
static ventWaitHandle waitHandle =new AutoResetEvent(false);
static void Main(){
new Thread(Waiter).start();
Thread.sleep(1000);
waitHandle.set();
}
static void Waiter(){
Console.WriteLine("Waiting...");
waitHandle.WaitOne();
Console.WriteLine("Notified")
}
}
双向信号
假设主线程需要向工作线程连续发送三次信号。如果主线程单纯地连续调用Set方法若干次,那么第二次或者第三次发送的信号就有可能丢失,因为工作线程需要时间来处理每一次的信号。
其解决方案是主线程等待工作线程准备就绪之后再发送信号。这可以通过引入另一个AutoResetEvent来实现两个线程交互执行,例如:
using System;
using System.Threading;
class Program
{
static AutoResetEvent signalA = new AutoResetEvent(false);
static AutoResetEvent signalB = new AutoResetEvent(false);
static void Main()
{
Thread threadA = new Thread(ThreadAFunction);
Thread threadB = new Thread(ThreadBFunction);
threadA.Start();
threadB.Start();
// 主线程等待一段时间,然后发送信号A
Thread.Sleep(1000);
Console.WriteLine("Main thread sending signal A to Thread B.");
signalA.Set();
// 等待一段时间,然后发送信号B
Thread.Sleep(1000);
Console.WriteLine("Main thread sending signal B to Thread A.");
signalB.Set();
threadA.Join();
threadB.Join();
}
static void ThreadAFunction()
{
Console.WriteLine("Thread A waiting for signal B.");
signalB.WaitOne();
Console.WriteLine("Thread A received signal B.");
// Thread A 执行操作
}
static void ThreadBFunction()
{
Console.WriteLine("Thread B waiting for signal A.");
signalA.WaitOne();
Console.WriteLine("Thread B received signal A.");
// Thread B 执行操作
}
}
ManuaIResetEvent
ManualResetEvent的作用就像是一个大门。调用Set方法就开启大门,并允许任意数目的调用WaitOne方法的线程通过大门。而调用Reset方法则会关闭大门。在大门关闭时调用WaitOne方法会发生阻塞。而当大门再次打开时,线程会立刻释放。除这些区别之外,ManualResetEvent的功能和AutoResetEvent是一样的。
和AutoResetEvent一样,创建ManualResetEvent的方法有两种:
var manual1=new ManualResetEventfalse); var manual2= new EventWaitHandle(false,EventResetMode.ManualReset);
CountdownEvent
CountdownEvent可用于等待多个线程。该类是在.NETFramework4.0引入的,并同样具有高效的纯托管实现。若使用该类,需要在实例化时指定线程数目或者需要等待的线程“计数”
var countdown =new CountdownEvent(3);
调用 Signal 会使计数递减;而调用Wait则会阻塞,直至计数减为零。例如:
static CountdownEvent countdown =new CountdownEvent(3);
static void Main(){
new Thread(SaySomething).start("I am thread 1");
new Thread(SaySomething).Start ("I am thread 2");
new Thread(SaySomething).start("I am thread 3");
countdown.Wait();//Blocks until Signal has been called 3 times
Console.WriteLine("All threads have finished speaking!");
}
static void SaySomething(object thing){
Thread.sleep(1000);
Console.WriteLine(thing);
countdown.Signal();
}
*创建跨进程的EventWaitHandle
ration);
writerThread.Start();
readerThread1.Start();
readerThread2.Start();
writerThread.Join();
readerThread1.Join();
readerThread2.Join();
Console.WriteLine("All threads completed.");
}
static void ReadOperation()
{
rwl.EnterReadLock();
Console.WriteLine("Reading resource value: " + resource);
Thread.Sleep(1000); // Simulate reading
rwl.ExitReadLock();
}
static void UpgradeableReadOperation()
{
rwl.EnterUpgradeableReadLock();
Console.WriteLine("Upgradeable read: " + resource);
// Example of upgrading to write lock conditionally
if (resource == 0)
{
rwl.EnterWriteLock();
resource = 1; // Simulate an update
rwl.ExitWriteLock();
}
rwl.ExitUpgradeableReadLock();
}
static void WriteOperation()
{
rwl.EnterWriteLock();
Console.WriteLine("Writing to resource");
resource++;
Thread.Sleep(1000); // Simulate writing
rwl.ExitWriteLock();
}
}
#### *递归锁
## 使用时间等待句柄发送信号
> 最简单的信号发送结构是事件等待句柄(eventwaithandles)。注意它和C#的事件是无关的。事件等待句柄有三种实现:AutoResetEvent、ManualResetEvent(Slim)和CountdownEvent。前两种基于通用的EventWaitHandle类,它们继承了基类的所有功能。
### AutoResetEvent
> AutoResetEvent就像验票机的闸门一样:插入一张票据只允许一个人通过。其名称中的 Auto 指的是开放的闸机在行人通过后会自动关闭或重置。线程可以调用WaitOne方法在闸机门口等待、阻塞(在“一个”闸机前等待,直至闸机门开启)。调用Set方法即向闸机中插入一张票据。如果有一系列的线程调用了WaitOne,那么它们会在闸机后排队等待注2。票据可以来自任何线程,即任何一个能够访问AutoResetEvent对象的非阻塞线程都可以调用Set方法来释放一个阻塞的线程
*创建AutoResetEvent的方法有两种。第一种是使用其构造器*
```cs
var auto =new AutoResetEvent(false);
(如果在构造器中以true为参数,则相当于立刻调用Set方法)。
第二种方法则是使用如下方式创建AutoResetEvent:
var auto =new EventWaitHandle(false,EventResetMode.AutoReset);
[外链图片转存中…(img-OnBnmIf1-1721286391984)]
class BasicWaitHandle{
static ventWaitHandle waitHandle =new AutoResetEvent(false);
static void Main(){
new Thread(Waiter).start();
Thread.sleep(1000);
waitHandle.set();
}
static void Waiter(){
Console.WriteLine("Waiting...");
waitHandle.WaitOne();
Console.WriteLine("Notified")
}
}
双向信号
假设主线程需要向工作线程连续发送三次信号。如果主线程单纯地连续调用Set方法若干次,那么第二次或者第三次发送的信号就有可能丢失,因为工作线程需要时间来处理每一次的信号。
其解决方案是主线程等待工作线程准备就绪之后再发送信号。这可以通过引入另一个AutoResetEvent来实现两个线程交互执行,例如:
[外链图片转存中…(img-pQwH30fG-1721286391984)]
using System;
using System.Threading;
class Program
{
static AutoResetEvent signalA = new AutoResetEvent(false);
static AutoResetEvent signalB = new AutoResetEvent(false);
static void Main()
{
Thread threadA = new Thread(ThreadAFunction);
Thread threadB = new Thread(ThreadBFunction);
threadA.Start();
threadB.Start();
// 主线程等待一段时间,然后发送信号A
Thread.Sleep(1000);
Console.WriteLine("Main thread sending signal A to Thread B.");
signalA.Set();
// 等待一段时间,然后发送信号B
Thread.Sleep(1000);
Console.WriteLine("Main thread sending signal B to Thread A.");
signalB.Set();
threadA.Join();
threadB.Join();
}
static void ThreadAFunction()
{
Console.WriteLine("Thread A waiting for signal B.");
signalB.WaitOne();
Console.WriteLine("Thread A received signal B.");
// Thread A 执行操作
}
static void ThreadBFunction()
{
Console.WriteLine("Thread B waiting for signal A.");
signalA.WaitOne();
Console.WriteLine("Thread B received signal A.");
// Thread B 执行操作
}
}
ManuaIResetEvent
ManualResetEvent的作用就像是一个大门。调用Set方法就开启大门,并允许任意数目的调用WaitOne方法的线程通过大门。而调用Reset方法则会关闭大门。在大门关闭时调用WaitOne方法会发生阻塞。而当大门再次打开时,线程会立刻释放。除这些区别之外,ManualResetEvent的功能和AutoResetEvent是一样的。
和AutoResetEvent一样,创建ManualResetEvent的方法有两种:
var manual1=new ManualResetEventfalse); var manual2= new EventWaitHandle(false,EventResetMode.ManualReset);
CountdownEvent
CountdownEvent可用于等待多个线程。该类是在.NETFramework4.0引入的,并同样具有高效的纯托管实现。若使用该类,需要在实例化时指定线程数目或者需要等待的线程“计数”
var countdown =new CountdownEvent(3);
调用 Signal 会使计数递减;而调用Wait则会阻塞,直至计数减为零。例如:
static CountdownEvent countdown =new CountdownEvent(3);
static void Main(){
new Thread(SaySomething).start("I am thread 1");
new Thread(SaySomething).Start ("I am thread 2");
new Thread(SaySomething).start("I am thread 3");
countdown.Wait();//Blocks until Signal has been called 3 times
Console.WriteLine("All threads have finished speaking!");
}
static void SaySomething(object thing){
Thread.sleep(1000);
Console.WriteLine(thing);
countdown.Signal();
}
*创建跨进程的EventWaitHandle
原文地址:https://blog.csdn.net/m0_46319477/article/details/140523707
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!