【一文详解】知识分享:(C#开发学习快速入门)

面向对象(OOP)

c语言是面向过程。

c++是面向过程+面向对象。

c#是纯粹的面向对象: 核心思想是以人的思维习惯来分析和解决问题。万物皆对象

面向对象开发步骤:

  1. 分析对象

    • 特征
    • 行为
    • 关系(对象关系/类关系)
  2. 写代码:

    特征–>成员变量

    方法–>成员方法

  3. 实例化–具体对象

Note(补充知识):

  1. 类=模板

  2. (类我们一般用于定义新的数据类型)

    (定义好的类 = 新的数据类型,故可以用于定义对应类型变量)

  3. 类的成员分为普通成员和静态成员

  4. 类间关系:

    • 泛化(Generalization):
      【一文详解】知识分享:(C#开发学习快速入门)_第1张图片

    • 实现(Realization):

      【一文详解】知识分享:(C#开发学习快速入门)_第2张图片

    • 关联(Association):

      【一文详解】知识分享:(C#开发学习快速入门)_第3张图片

    • 聚合(Aggregation):

      【一文详解】知识分享:(C#开发学习快速入门)_第4张图片

    • 组合(Composition):
      【一文详解】知识分享:(C#开发学习快速入门)_第5张图片

    • 依赖(Dependency):

      【一文详解】知识分享:(C#开发学习快速入门)_第6张图片


.Net(framework)和C#

两者的关系:.Net Framework 的地位类似于 Java中的JVM.

【一文详解】知识分享:(C#开发学习快速入门)_第7张图片

c#语言的编译过程:

【一文详解】知识分享:(C#开发学习快速入门)_第8张图片

Note:

  1. C#的语言编译器: csc(c sharp compiler),其与.Net框架安装在一起,即在同一个安装路径下。

    例如,在我本机的路径是:C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe

数据类型

【一文详解】知识分享:(C#开发学习快速入门)_第9张图片

c# partial关键字

在 C# 中,您可以使用 partial 关键字在多个 .cs 文件中拆分类、结构、方法或接口的实现。

编译程序时,编译器将自动合并来自多个 .cs 文件的所有实现。


部分类:

示例如下:

EmployeeProps.cs

public partial class Employee
{
    public int EmpId { get; set; }
    public string Name { get; set; }
}

EmployeeMethods.cs

public partial class Employee
{
    //constructor
    public Employee(int id, string name)
    {
        this.EmpId = id;
        this.Name = name;
    }

    public void DisplayEmpInfo()
    {
        Console.WriteLine(this.EmpId + " " this.Name);
    }
}

上面,EmployeeProps.cs 包含了 Employee 类的属性,EmployeeMethods.cs 包含了 Employee 类的所有方法。

这些将被合并编译为一个 Employee 类。

Employee

public class Employee
{
    public int EmpId { get; set; }
    public string Name { get; set; }

    public Employee(int id, string name)
    {
        this.EmpId = id;
        this.Name = name;
    }

    public void DisplayEmpInfo()
    {
        Console.WriteLine(this.EmpId + " " this.Name );
    }
}

部分类的规则:

  • 所有部分类定义必须在相同的程序集和命名空间中。
  • 所有部分类必须具有相同的可访问性,例如公共或私有等。
  • 如果任何部分声明为抽象、密封或基类型,则整个类声明为相同类型。
  • 不同的部分类必须有相同的基类型。
  • Partial 修饰符只能出现在关键字 class、struct 或 interface 之前。
  • 允许嵌套的部分类。

什么时候会用到部分类:

通常是一个类太大,或者有一些类我们是用代码生成的。

然后我们自己的方法代码不希望影响到开源软件生成的代码 ,即我们不希望影响到旧的代码。这样维护的时候很麻烦。

部分方法拆分为分部类或结构的两个单独的 .cs 文件。

两个 .cs 文件之一包含方法的签名,另一个文件可以包含这个部分方法的实现。且 方法的声明和实现都必须有 partial 关键字。

EmployeeProps.cs

public partial class Employee{
    public Employee(){
        GenerateEmpId();
    }
    
    public Guid EmpId{get;set;}
    public string name{get;set;}
    
    partial void GenerateEmpId();
}

EmployeeMethods.cs

public partial class Employee{
    partial void GenerateEmpId(){
        this.EmpId  = 	Guid.NewGuid();
    }
}

部分方法的规则:

  • 部分方法必须使用 partial 关键字

  • 部分方法如果不是void的话,则必须加访问修饰符。

  • 部分方法可以使用 in, ref, out 来修饰参数。

  • 部分方法是隐式私有方法,因此不能是虚拟的。

不支持virtual

  • 部分方法可以是静态方法。

  • 部分方法可以是泛型

c# using关键字

在 C# 中,using 关键字有两个主要的用途:

1.资源管理(Resource Management): using 用于在代码块执行完毕后释放资源,确保对象在离开作用域时被正确清理。这通常用于实现 IDisposable 接口的对象,如文件流、数据库连接等。

using (FileStream fs = new FileStream("example.txt", FileMode.Open))
{
    // 使用文件流进行操作
    // 在这个代码块结束时,FileStream 会被自动关闭和释放资源
}

2.导入命名空间(Namespace Import): using 也用于导入命名空间,使得在代码中可以直接使用命名空间中的类型,而不需要使用完全限定名。提高代码的可读性.

//导入命名空间
using NamespaceName;

注意:

using 关键字用于导入命名空间,但它只导入指定的命名空间,不会自动导入该命名空间下的所有子命名空间

例如,如果你使用了 using System; 导入了 System 命名空间,那么只有 System 命名空间下的类型会被导入,而 System.ThreadingSystem.IO 等子命名空间下的类型不会自动导入。

如果需要使用子命名空间的类型,你需要额外使用 using 导入相应的子命名空间。这样可以根据需要有选择地导入所需的命名空间,而不会导入整个命名空间的所有内容,从而保持代码的简洁性和可维护性。

c# var和dynamic关键字

var关键字

var 是 C# 中的一个关键字,用于在声明变量时让编译器根据初始化表达式的类型来自动推断变量的类型。

使用 var 关键字可以使代码更为简洁,尤其是在处理复杂的类型或匿名类型时。

  1. 基本用法:
var number = 42; // 推断为 int 类型
var name = "John"; // 推断为 string 类型
var price = 19.99; // 推断为 double 类型
  1. 在集合中使用:
var numbers = new List { 1, 2, 3, 4, 5 };
foreach (var num in numbers)
{
    // num 的类型被推断为 int
    Console.WriteLine(num);
}
  1. 匿名类型(Anonymous Types):
var person = new { Name = "John", Age = 30 };
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
  1. LINQ 查询中的匿名类型:
var result = from p in people
             where p.Age > 21
             select new { p.Name, p.Age };
  1. 在方法中使用:
var result = Add(3, 5);

int Add(int a, int b)
{
    return a + b;
}
  1. LINQ 方法语法查询中的 var:
var adults = people.Where(p => p.Age >= 18).ToList();

注意事项:

  • var 不是一种动态类型,而是在编译时确定类型的隐式类型。

  • 在声明变量时使用 var 时,必须在同一行进行初始化。

    var i = 10;
    

    否则 C# 编译器会报错:必须初始化隐式类型的变量。

    var i; // 编译错误: 隐式类型化的变量必须已初始化 Implicitly-typed variables must be initialized
    i = 100;
    
  • 使用 var 时,编译器会根据初始化表达式的类型进行类型推断。

  • 不允许使用一个var 定义多个变量

    var i = 100, j = 200, k = 300; // Error: 隐式类型化的变量不能有多个声明符
    
  • var不能用在方法的形参参数上面

    void Display(var param) //编译错误
    {
        Console.Write(param);
    }
    
  • var 可以用在for 和 foreach的循环当中

    for(var i = 0;i < 10;i++){
        Console.WriteLine(i);
    }
    
  • var 还可以被用在 linq的查询当中

    特别是在linq查询当中如果返回类型是匿名类型的时候,也只能用var了。

    var stringList = new List(){
        "C# 教程",
        "VB.NET 教程",
        "Learn C++",
        "MVC 教程" ,
        "Java" 
    };
    
    //Linq查询语法,(跟sql很像)
    var result = from s in stringList
        		where s.Contains("教程")
        		select s;
    

dynamic关键字

在C#中,dynamic 是一种在运行时执行类型检查的类型。使用 dynamic 声明的变量可以在运行时改变其类型,而不是在编译时确定。这种灵活性使得 dynamic 在处理与运行时相关的场景时非常有用。

  1. 基本用法:
dynamic dynamicVariable = 10;
Console.WriteLine(dynamicVariable); // 输出 10

dynamicVariable = "Hello";
Console.WriteLine(dynamicVariable); // 输出 Hello

在这个例子中,dynamicVariable 可以在运行时切换为不同的类型。

  1. 与反射结合使用:
Assembly assembly = Assembly.LoadFile("Example.dll");
dynamic instance = assembly.CreateInstance("Example.MyClass");

// 调用 MyClass 中的方法
instance.MyMethod();

dynamic 可以与反射结合使用,使得在运行时访问和调用类型的成员更为方便。

  1. 作为方法的参数或返回类型:
public dynamic GetDynamicResult()
{
    return DateTime.Now.Second % 2 == 0 ? 42 : "Hello";
}

dynamic result = GetDynamicResult();
Console.WriteLine(result); // 输出 42 或 Hello

dynamic 可以作为方法的参数或返回类型,使得方法的行为在运行时更加灵活。

需要注意的是,dynamic 是一种牺牲了编译时类型检查的灵活性,因此在使用时需要小心,并确保在运行时不会导致类型错误。

在大多数情况下,应尽量使用静态类型以便在编译时发现潜在的错误。


var和dynamic的对比

对于var关键字:

  • 编译时类型推断: var 用于在编译时根据初始化表达式的类型推断变量的类型。

  • 静态类型: 一旦类型确定,变量就成为该类型的静态类型,后续不能更改。

  • 局部变量: var 通常用于声明局部变量,如方法内的变量。

  • 不能用于声明成员变量: var 不能用于声明类的成员变量,因为var需要初始化表达式,而类的成员变量不会初始化表达式。

对于dynamic关键字:

  • 运行时类型推断: dynamic 用于在运行时推断变量的类型。类型检查是在运行时而不是在编译时进行的。
  • 动态类型: dynamic 声明的变量是动态类型的,可以在运行时更改其类型。
  • 可用于声明成员变量: dynamic 可以用于声明类的成员变量,因为它不要求在声明时就指定类型和初始化表达式。

c# new关键字

c#中的new关键字作用:

  1. 要么可以在实例化一个对象时使用,如下示例:

    int[] numbers = new int[5];// 使用 new 初始化整数数组,默认值为 0 
    
  2. 要么可以隐藏基类(父类)成员时使用:

    在 C# 中,new 关键字用于明确表示你正在故意隐藏父类的成员。

    不加 new 也可以隐藏父类的成员,但会产生编译器警告,提醒你可能是无意中隐藏了父类的成员。

    class Parent
    {
        public void Display()
        {
            Console.WriteLine("Parent Display");
        }
    }
    
    class Child : Parent
    {
        //不加new,编译器会发出警告,因为 Child 类中的 Display 方法没有使用 new 或 override 关键字,可能会无意隐藏了父类的成员。然而,程序仍然可以编译和运行。
        public void Display()
        {
            Console.WriteLine("Child Display");
        }
    }
    
    ----------------------------------------------------
    class Parent
    {
        public void Display()
        {
            Console.WriteLine("Parent Display");
        }
    }
    
    class Child : Parent
    {
        //使用了new
        //在这种情况下,使用了 new 关键字,明确表示是有意隐藏父类的成员。
        //编译器将不再发出警告,并且程序可以正常编译和运行。
        public new void Display()
        {
            Console.WriteLine("Child Display");
        }
    }
    

c# out关键字

使用 out 关键字的主要原因是为了允许方法返回多个值。在 C# 中,方法只能返回一个值。

但是,如果你需要从方法中返回多个值,你可以将其中一个或多个值作为 out 参数传递给该方法。

总之,out 关键字的作用是允许方法返回多个值,因为它允许在方法内部修改参数的值,并在方法完成后将修改后的值传递回调用方。

以下是一个简单的示例,展示了如何使用 out 关键字:

public void Calculate(int input, out int output)
{
    // 在这里进行计算
    output = 42; // 将计算结果保存在输出参数中
}

// 调用 Calculate 方法
int result;
Calculate(10, out result);
Console.WriteLine(result); // 输出 42

//在上面的示例中,Calculate 方法接受一个整数作为输入,并将计算结果保存在 output 参数中。
//在调用该方法时,我们将一个变量作为 out 参数传递给该方法,以便在方法完成后获取计算结果。

我的理解: 使用out关键字后,传递进去的实参经过方法内部修改后也会更新保存到这个实参中。

//这个例子中,CalculateSumAndDifference 方法接受两个整数 a 和 b,并使用两个 out 参数 sum 和 difference 返回它们的和和差。在调用这个方法时,传递给方法的 sum 和 difference 参数实际上是用于接收输出的变量。

//通过这种方式,你可以使用 out 参数在一个方法中返回多个值。
//注意,out 参数在调用方法前不需要初始化,因为方法会在内部为这些参数赋值。
class Program
{
    static void Main()
    {
        int inputA = 5;
        int inputB = 3;
        int sum;
        int difference;
		//调用时方法要传递out
        CalculateSumAndDifference(inputA, inputB, out sum, out difference);

        Console.WriteLine($"Sum: {sum}, Difference: {difference}");
    }
	//声明时方法也要传递out
    static void CalculateSumAndDifference(int a, int b, out int sum, out int difference)
    {
        sum = a + b;
        difference = a - b;
    }
}

属性

  • c#中,类中的属性和方法, 若没有定义public等权限修饰符时,默认是private,只能在类内进行访问。
  • 若要跨类访问,可以修改为public的权限修饰符。如: public float size;但直接修改为public属性的缺点是外部的类可以任意修改一个类的属性值,所以遇到这种情况,在Java中的解决方案是外部类利用类内的getter和setter方法去访问和修改类的成员变量,同时类内的getter和setter方法也可在方法中进行一定的逻辑处理和限制,从而达到不让外部类随便改类内字段值的目的。
  • C#中,则是利用访问器/属性解决这个问题,其思想也类似于Java的POJO类中的getter和setter。

常规属性

什么是常规属性?

答: 在类中,为了安全起见,一般把数据成员设置为private

即先定义一个private的私有的字段,然后再为这个私有字段封装一个public公开的属性,在属性中实现 get 和 set 两个方法,这种方式叫做常规属性

注意: private权限的变量命名一般首字母小写。

​ 而public权限的变量命令一般首字母大写。

常规属性有什么作用?

答:C#中常规属性用于对类中成员变量的取值或赋值进行限制过滤。

示例如下:

class demo{
    //先定义一个私有的字段
    private float size;
    //然后再为这个私有字段封装一个公开的属性,在属性中实现 get 和 set 两个方法
    //常规属性用于对类中成员变量的取值或赋值进行限制过滤。**
    public float Size{
        get{return this.size};
        set{this.size = value};
    }
}

//上面的示例中,我们将`size`这个字段的权限修饰符由`public`改为`private`,同时定义了一个属性代码`public float Size{}`,里面 的`get{}`代码段用于返回属性值,其中的`set{}`代码段用于设置值,且其中的`value`是保留字,其代表了外界赋给这个字段的值

自动属性

什么情况下使用自动属性?

在某些情况下,属性的 get 和 set 只是完成字段的取值和赋值操作,而不包含任何附加的逻辑代码,这个时候可以使用自动属性。

什么是自动属性?

答: 是微软提供的一种快速简洁建立属性的方式。总的来说,是一种相比常规属性而言更加简洁的写法。

如:public int Age { get; set; }

其相当于get{}代码段里面默认写好了return某个字段,set{}代码段里面默认写好了this.xx字段=value。

同时,我们使用自动属性的时候,就不需要再写对应的私有字段了,C#编译器会自动给我们的自动属性提供一个对应的字段,即不用手工声明一个私有的字段。

注意:

  1. 如果是想只能只读/只能只写,自动属性代码如何写?如下:

    加个private关键字从而达到限制的目的。

    【一文详解】知识分享:(C#开发学习快速入门)_第10张图片

  2. 自动属性同时实现get和set访问器,不能使用自动属性只实现其中的一种,

    public String Name { get; }这种写法会报错的。

  3. 在vs IDE中输入prop ,点击两次tab会生成一个默认的自动属性

    【一文详解】知识分享:(C#开发学习快速入门)_第11张图片

两种属性对比

对比常规属性自动属性:

  • 自动属性写起来的更快,且不用自己写get,set方法,直接使用默认的;

  • 两者来说,可能自动属性对比常规属性,可能会少些两句代码,但不如常规属性自己写的方法灵活,按需使用。


字段和属性的异同

  1. 访问方式:

    • 字段(Field): 是类中的变量,直接存储数据。

      可以通过类的实例直接访问,但一般来说不鼓励直接暴露字段,因为这会破坏封装性。

      class MyClass
      {
          public int myField; // 字段
      }
      
    • 属性(Property): 提供了一种更加控制和灵活的方式来访问或修改类的私有字段的值

      属性具有读取器(getter)和写入器(setter),可以在读取和写入数据时执行自定义逻辑。

      class MyClass
      {
          private int myField; // 私有字段
      
          public int MyProperty // 属性
          {
              get { return myField; }
              set { myField = value; }
          }
      }
      
  2. 封装性:

    • 字段(Field): 通常是类的内部实现细节,可以通过直接暴露为 public 或 internal 字段来使其对外可见,但这样做会破坏封装性。
    • 属性(Property): 提供了更好的封装性,允许类作者在保护类内部实现的同时,对外提供更安全和灵活的访问接口。
  3. 使用场景:

    • 字段(Field): 适用于不需要添加逻辑的简单数据存储,或者在某些情况下需要对字段进行直接访问的场景。
    • 属性(Property): 适用于需要在读取或写入数据时执行一些逻辑、验证或计算的场景。它提供了更灵活的控制和更好的封装性。

在实际的编程中,一般建议使用属性而不是直接暴露字段,以便更好地控制对类内部实现的访问。

C# 中的自动属性(Auto-implemented Properties)提供了一种更简洁的语法来声明属性,省去了手动实现 get 和 set 方法的步骤。

class MyClass
{
    public int MyProperty { get; set; } // 自动属性
}
//在这个例子中,`MyProperty` 是一个公共的自动属性,编译器会`自动生成相应的私有字段以及默认的 get 和 set 方法`。

委托

what

  • 在C#中,委托是一种能够存储对方法的引用的类型。

    它允许您将方法作为参数传递给其他方法将方法存储在变量中,委托可以被认为是函数指针

  • 通过委托,你可以像传递变量一样传递方法, 它可以使你像传递参数一样传递方法

  • 主要用于实现回调机制、事件处理和委托链等。

委托定义了一个方法签名,它指定了可以由该委托引用的方法的返回类型和参数列表

委托定义的方法签名定义了它所能代表的方法种类。

可以将任何符合该方法签名的方法分配给该委托,使其成为它的实例。

一旦将方法分配给委托,您可以像调用方法一样调用该委托,以便在调用委托时执行该方法

使用委托的作用:

1、避免核心方法中存在大量的if…else…语句(或swich开关语句);

2、满足程序设计的OCP原则;

3、使程序具有扩展性;

4、绑定事件;

5、结合Lambda表达式,简化代码,高效编程;

6、实现程序的松耦合(解耦),这个在事件(event)中体现比较明显;


**委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)**语句,同时使得程序具有更好的可扩展性。


委托是对函数的引用,如果我们调用委托,实际是调用的委托引用的函数。委托拥有一个函数或一组函数的所有必要信息,包括签名和返回值类型。


静态设计时,当我们不知道具体哪个函数会被调用时,我们可以使用委托。

定义

delegate关键字定义委托,定义委托,它定义了可以代表的方法的类型。

(注意,委托是没有方法体的,类似接口里面的方法)

在定义委托前,必须明确两个问题:

1、委托将要绑定的方法;

2、委托的形参类型,形参个数和委托的返回值必须与将要绑定的方法的形参类型,形参个数和返回值一致

public delegate 委托返回类型 委托名(形参)


// 定义委托
delegate string MyDelegate(string message);


调用

可以使用 Invoke() 方法或使用 () 运算符调用委托。

如下示例:

public delegate void MyDelegate(string message);//声明一个委托

class Program{
    // 跟MyDelegate有相同参数和返回值的方法 称为签名相同
    static void MethodA(string message)
    {
        Console.WriteLine(message);
    }
    
    static void Main(string[] args){
        //把函数传给委托
        MyDelegate md = new MyDelegate(MethodA);
        //或者直接赋值给委托
        MyDelegate md2 = MethodA;
        //使用Lambda表达式
        MyDelegate  md3 = (string message) => Console.WriteLine(message);
        
        //设置目标方法后,可以使用 Invoke() 方法或使用 () 运算符调用委托。
        md.Invoke("hello");
        md2("world");
        md3("hello world");
    }
}

操作/种类

注意:

在 C# 中,如果一个委托变量没有绑定任何具体的方法,它的值将为 null

1、单播委托绑定方法:绑定单个方法

  • 直接传递方法名称绑定对应的方法和调用委托,如下:
//定义 委托
public delegate void GreetingDelegate(string name);

//具体方法1
private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}
//具体方法2
private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
}

 //调用委托的方法,注意此方法,它接受一个GreetingDelegate类型的方法作为参数
private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
}

//直接传递方法名称调用委托(如同调用方法)
GreetPeople("Jimmy Zhang", EnglishGreeting);
GreetPeople("张子阳", ChineseGreeting);
  • 先声明委托变量,再使用=给委托变量赋值具体方法

    最后传递委托变量来调用委托, 如下:

//定义 委托
public delegate void GreetingDelegate(string name);

//具体方法
private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}
//具体方法
private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
}

 //注意此方法,它接受一个GreetingDelegate类型的方法作为参数
private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
}

//声明委托变量
GreetingDelegate delegate1, delegate2;
//委托变量赋值:委托变量绑定具体方法
delegate1 = EnglishGreeting;
delegate2 = ChineseGreeting;

//调用委托(如同调用方法)
GreetPeople("Jimmy Zhang", delegate1);
GreetPeople("张子阳", delegate2);
  • 也可以绕过GreetPeople方法,通过委托来直接调用EnglishGreeting和ChineseGreeting:
//定义 委托
public delegate void GreetingDelegate(string name);

//具体方法
private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}
//具体方法
private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
}

//声明委托变量
GreetingDelegate delegate1;

//委托变量赋值
delegate1 = EnglishGreeting; // 先给委托类型的变量赋值
delegate1 += ChineseGreeting;   // 给此委托变量再绑定一个方法

//调用委托
//将先后调用 EnglishGreeting 与 ChineseGreeting 方法
delegate1 ("Jimmy Zhang");  

2、多播委托绑定方法:利用+=绑定多个方法

可以将多个方法赋给同一个委托,或者叫将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其所绑定的方法

注意:

  • 绑定多个方法时,委托范围类型必须为void类型,否则只返回最后一个绑定的值。
  • 使用委托可以将多个方法绑定到同一个委托变量,当调用此变量时(这里用“调用”这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。

  • 可利用第一个方法用=形式,其余方法用+= 的形式来进行多播委托,如下:

注意这里,第一次用的“=”,是赋值的语法;第二次,用的是“+=”,是绑定的语法。
如果第一次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。

//定义 委托
public delegate void GreetingDelegate(string name);

//具体方法
private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}
//具体方法
private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
}

//调用委托: 注意此方法,它接受一个GreetingDelegate类型的方法作为参数
private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
}

//声明委托变量
GreetingDelegate delegate1;


//委托变量绑定具体方法
//注意这里,第一次用的“=”,是赋值的语法;第二次,用的是“+=”,是绑定的语法。
//如果第一次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。
delegate1 = EnglishGreeting;// 先用=给委托类型的变量赋值一个方法
delegate1 += ChineseGreeting;// 再用+=给此委托变量再绑定一个方法

//调用委托(如同调用方法)
//将先后调用 EnglishGreeting 与 ChineseGreeting 方法
GreetPeople("Jimmy Zhang",delegate1);
  • 也可以使用下面的代码来这样简化其中的多播委托过程:
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);//先利用类似构造函new委托变量形式传递绑定一个方法
delegate1 += ChineseGreeting;  // 给此委托变量再绑定一个方法
-----------------------------------------------------------------------------------------------
你不禁想到:上面第一次绑定委托时不可以使用“+=”的编译错误,或许可以用这样的方法来避免:

GreetingDelegate delegate1 = newGreetingDelegate();
delegate1 += EnglishGreeting;   // 这次用的是 “+=”,绑定语法。
delegate1 += ChineseGreeting;   // 给此委托变量再绑定一个方法

但实际上,这样会出现编译错误: 因为“GreetingDelegate”所代表方法没有采用“0”个参数的重载。有的话还可以尝试下。

所以之后总的代码就变成,如下:

//定义 委托
public delegate void GreetingDelegate(string name);

//具体方法
private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}
//具体方法
private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
}

//调用委托: 注意此方法,它接受一个GreetingDelegate类型的方法作为参数
private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
}


//先利用类似构造函new委托变量形式传递绑定一个方法
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
//给此委托变量再绑定一个方法
delegate1 += ChineseGreeting;  

//调用委托(如同调用方法)
//将先后调用 EnglishGreeting 与 ChineseGreeting 方法
GreetPeople("Jimmy Zhang",delegate1);

3、解绑方法:利用-=解绑方法

//定义 委托
public delegate void GreetingDelegate(string name);

//具体方法1
private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}
//具体方法2
private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
}

//调用委托: 注意此方法,它接受一个GreetingDelegate类型的方法作为参数
private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
}


//先利用类似构造函new委托变量形式传递绑定一个方法
GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
//给此委托变量再绑定一个方法
delegate1 += ChineseGreeting;  

//调用委托(如同调用方法)
//将先后调用 EnglishGreeting 与 ChineseGreeting 方法
GreetPeople("Jimmy Zhang",delegate1);


//利用-=解绑方法
delegate1 -= EnglishGreeting; //取消对EnglishGreeting方法的绑定
// 将仅调用 ChineseGreeting
GreetPeople("张子阳", delegate1);

普通委托

示例demo1.cs:

namespace test{
     //定义委托,它定义了可以代表的方法的类型
    public delegate void GreetingDelegate(string name);
    
   	//新建的GreetingManager类
    public class GreetingManager{
        //包含调用委托的方法
       public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
           MakeGreeting(name);//调用委托
       }
    }
    
    class Program{
        //具体方法1
        private static void EnglishGreeting(string name) {
            Console.WriteLine("Morning, " + name);
        }
        //具体方法2
        private static void ChineseGreeting(string name) {
            Console.WriteLine("早上好, " + name);
        }
        
        //Main
        static void Main(string[] args){
            //创建类的实例对象
            GreetingManager greetingManager = new GreetingManager();
            //给调用委托的方法传入具体的方法
            greetingManager.GreetPeople("Jimmy Zhang",EnglishGreeting);
            greetingManager.GreetPeople("章三",ChineseGreeting);
        }
    }
    
}

现在,假设需要将多个具体方法绑定到同一个委托变量,该如何做呢?再次改写代码,如下:

namespace test{
     //定义委托,它定义了可以代表的方法的类型
    public delegate void GreetingDelegate(string name);
    
   	//新建的GreetingManager类
    public class GreetingManager{
        //包含调用委托的方法
       public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
           MakeGreeting(name);//调用委托
       }
    }
    
    class Program{
        //具体方法1
        private static void EnglishGreeting(string name) {
            Console.WriteLine("Morning, " + name);
        }
        //具体方法2
        private static void ChineseGreeting(string name) {
            Console.WriteLine("早上好, " + name);
        }
        
        //Main(客户端)
        static void Main(string[] args){
            //创建类的实例对象
            GreetingManager greetingManager = new GreetingManager();
            //声明委托变量
            GreetingDelegate delegate1;
            //给委托变量进行多播委托绑定方法
            delegate1 = EnglishGreeting;
            delegate1 += ChineseGreeting;
            //调用委托(相当于调用方法)
            greetingManager.GreetPeople("Jimmy Zhang",delegate1); 
        }
    }   
}

既然可以声明委托类型的变量(在上例中是delegate1),为何不将这个变量封装到 GreetManager类中?

这样客户端就不用去手动声明委托变量了,改写代码后的结果如下:

namespace test{
     //定义委托,它定义了可以代表的方法的类型
    public delegate void GreetingDelegate(string name);
    
   	//新建的GreetingManager类
    public class GreetingManager{
        //声明委托变量
        public GreetingDelegate delegate1;
        //包含调用委托的方法
       public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
           MakeGreeting(name);//调用委托
       }
    }
    
    class Program{
        //具体方法1
        private static void EnglishGreeting(string name) {
            Console.WriteLine("Morning, " + name);
        }
        //具体方法2
        private static void ChineseGreeting(string name) {
            Console.WriteLine("早上好, " + name);
        }
        
        //Main(客户端)
        static void Main(string[] args){
            //创建类的实例对象
            GreetingManager greetingManager = new GreetingManager();
            //给类中的委托变量属性进行多播委托绑定方法
            greetingManager.delegate1 = EnglishGreeting;
            greetingManager.delegate1 += ChineseGreeting;
            //调用委托(相当于调用方法)
            greetingManager.GreetPeople("Jimmy Zhang",greetingManager.delegate1); 
        }
    }   
}

尽管这样做没有任何问题,但我们发现这条语句写起来很奇怪。

即在调用greetingManager.GreetPeople方法的时候,传递了greetingManager的delegate1字段。

既然如此,我们修改 GreetingManager 类成这样:

//新建的GreetingManager类
public class GreetingManager{
    //声明委托变量
    public GreetingDelegate delegate1;
    //包含调用委托的方法
    public void GreetPeople(string name) {
        if(delegate1!=null){     //如果有方法注册委托变量
            delegate1(name);      //通过委托调用方法
        }
    }

然后修改后的最终代码如下:

namespace test{
     //定义委托,它定义了可以代表的方法的类型
    public delegate void GreetingDelegate(string name);
    
    //新建的GreetingManager类
    public class GreetingManager{
        //声明委托变量
        public GreetingDelegate delegate1;
        //包含调用委托的方法
        public void GreetPeople(string name) {
            if(delegate1!=null){     //如果有方法注册委托变量
                delegate1(name);      //通过委托调用方法
            }
        }
    
    class Program{
        //具体方法1
        private static void EnglishGreeting(string name) {
            Console.WriteLine("Morning, " + name);
        }
        //具体方法2
        private static void ChineseGreeting(string name) {
            Console.WriteLine("早上好, " + name);
        }
        
        //Main(客户端)
        static void Main(string[] args){
            //创建类的实例对象
            GreetingManager greetingManager = new GreetingManager();
            //给类中的委托变量属性进行多播委托绑定方法
            greetingManager.delegate1 = EnglishGreeting;
            greetingManager.delegate1 += ChineseGreeting;
            //调用委托(相当于调用方法)
            greetingManager.GreetPeople("Jimmy Zhang"); 
        }
    }   
}
  • 普通委托还可以利用lambda表达式简化写法,如下:
//定义委托
delegate void SendMessage(string text);

//原本的一个具体的函数(委托要引用的函数)
1 void WriteText(string text)
2 {
3     Console.WriteLine($"Text:{text}");
4 }
//原先委托变量赋值具体方法的写法1
SendMessage delegate1 = new SendMessage(WriteText);
//原先委托变量赋值具体方法的写法2
SendMessage delegate2 = WriteText;

//-------------------------------------------
//而利用lambda表达式简化写法后,变为如下的简洁写法:
SendMessage delegate3 = (text) => {Console.WriteLine($"Text:{text}");};

泛型委托

使用泛型委托可以为方法的参数类型和返回类型提供更大的灵活性和重用性,尤其是在编写通用代码时非常有用。


C#中定义泛型委托的语法:

delegate ();

//如果委托的定义符合一定的格式规范,可以省略 delegate 关键字


其中, 表示委托所表示方法的返回类型, 是委托的名称, 表示泛型类型参数, 是泛型方法的参数列表。


以下是一个示例,展示如何定义一个简单的泛型委托类型:

delegate T MyGenericDelegate(T x);
//这个泛型委托类型的名称是 MyGenericDelegate,它表示一个方法,该方法接受一个泛型类型的参数 T 并返回一个相同T类型的值。

下面是一个使用泛型委托的示例:

// 假设有一个泛型委托,用于处理任意类型的数据
delegate void ProcessDataDelegate(T data);

// 假设有一个类用于处理整数数据
class IntegerProcessor
{
    //具体的形参类型和具体方法
    public static void ProcessInteger(int number)
    {
        Console.WriteLine("Processing integer: " + number);
    }
}

// 假设有一个类用于处理字符串数据
class StringProcessor
{
    //具体的形参类型和具体方法
    public static void ProcessString(string text)
    {
        Console.WriteLine("Processing string: " + text);
    }
}

//client
class Program
{
    static void Main()
    {
        // 创建泛型委托实例,指向整数处理方法
        ProcessDataDelegate intDelegate = IntegerProcessor.ProcessInteger;

        // 创建泛型委托实例,指向字符串处理方法
        ProcessDataDelegate stringDelegate = StringProcessor.ProcessString;

        // 使用泛型委托处理整数数据
        intDelegate(10);

        // 使用泛型委托处理字符串数据
        stringDelegate("Hello, world!");
    }
}
//在上面的示例中,首先定义了一个泛型委托 ProcessDataDelegate,它可以处理任意类型的数据。
//然后,通过创建委托实例并将其指向不同的处理方法,分别处理整数和字符串数据。
//最后,通过调用委托实例,将数据传递给相应的处理方法进行处理。

预定义委托类型

在 C# 中,有一些预定义的委托类型,它们属于 System 命名空间。

这些委托类型提供了对常见委托签名的快捷方式,减少了在声明委托时需要手动编写的代码。

以下是一些常见的预定义委托类型:

1.Action 委托

Action 委托表示不返回值的方法。它可以接受从 0 到 16 个输入参数。

例如,Action 表示一个接受整数和字符串两个参数的方法,不返回值。

Action myAction = (x,s) => {
    Console.WriteLine($"Received int:{x},string:{s}");
};

myAction(42,"hello");

2.Func 委托

Func 委托表示有返回值的方法。它可以接受从 0 到 16 个输入参数,并且最后一个参数表示返回值类型

例如,Func 表示一个接受整数和字符串两个参数的方法,返回一个布尔值。

Func myFunc = (x, s) =>
{
    Console.WriteLine($"Received int: {x}, string: {s}");
    return true;
};

bool result = myFunc(42, "Hello");

3.Predicate 委托

PredicateFunc 的一个特殊版本,用于表示只接受一个参数并返回布尔值的方法

通常用于检查某个条件是否满足。

//由于一定返回bool值,故这里只填写一个int参数表明接收的那一个参数的类型
Predicate isEven = x => x % 2 == 0;

bool result = isEven(4);  // 返回 true

4.Comparison 委托

ComparisonFunc 的一个特殊版本,用于表示比较两个对象的方法。通常用于排序算法中。

//这个例子中,Comparison 表示一个接受两个 int 类型参数的方法,返回一个整数。
Comparison compareInts = (x, y) => x.CompareTo(y);

int result = compareInts(5, 3);  // 返回 1,表示第一个参数大于第二个参数;-1则第一个参数小于第二个参数

匿名类型

在C#中,**匿名类型(Anonymous Types)**是一种用于创建临时对象的特殊类型。

匿名类型允许你在不显式定义类的情况下,创建包含一组只读属性的对象。

这些属性的名称和类型由编译器根据初始化表达式进行推断。

例子:

var person = new { Name = "John", Age = 30 };

在这个例子中,person 是一个匿名类型的对象,它有两个只读属性 NameAge,这些属性的名称和类型由初始化表达式 "John"30 推断而来。

特点和限制:

  1. 只读属性: 匿名类型的属性是只读的,无法在初始化后修改。
  2. 属性推断: 属性的名称和类型是由初始化表达式推断而来的,而不是显式声明的。
  3. 编译时类型: 对于不同的初始化表达式,编译器会创建不同的匿名类型,即使属性的名称和类型相同。

匿名类型通常在需要在一个地方使用临时数据结构而不想专门为之创建一个类时很有用

示例1:例如在LINQ查询中选择特定的字段。

//LINQ查询(声明语法)
var query = from p in people
            select new { p.Name, p.Age };
//在这个例子中,`query` 是一个匿名类型的集合,包含了 `people` 集合中每个对象的 `Name` 和 `Age` 属性。

示例2:

var person = new { Name = "John", Age = 30 };

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

//这个例子中,`person` 是一个匿名类型的对象,可以通过属性名访问其属性值。

匿名函数

在C#中,匿名函数是一种无需命名的函数,可以在代码中定义和使用。

匿名函数通常用作委托的实际参数,以便在运行时将其传递给其他函数或方法。


C#中有两种类型的匿名函数:lambda表达式和匿名方法。


匿名方法

匿名方法是一种匿名函数。

匿名方法是一种使用delegate关键字定义的无需命名的方法。

它的语法如下:

delegate (parameter_list) { statements }


其中,parameter_list是一个用逗号分隔的参数列表,statements是一系列语句。

例如,以下代码创建了一个匿名方法,将两个整数相加并返回结果:

Func add = delegate(int x, int y) { return x + y; };//其中的Func是泛型委托,它表示一个能够接受两个整数参数并返回一个整数结果的委托
//上面的代码也可写成: Func add = delegate(x,y) {return x+y;}
//即可以省略形参的类型,因为通用委托Func中已经表明了方法形参的数据类型
int result = add(3, 4); // result = 7

lambda表达式

Lambda表达式是一种匿名函数,它可以快速地创建一个委托或表达式。

lambda 表达式是从匿名方法演变而来,首先删除了委托关键字(delegate)和参数类型,并添加了一个 lambda 运算符 =>

它的语法如下:

(parameter_list) => {expression}

  • 如果只有一个参数,参数列表(parameter_list)的()可以省略。

  • 如果方法体只有一行代码,方法体{expression}中可省略{}。

  • 如果=>之后的方法体中只有一行代码,且方法有返回值,那么可以省略方法体的{}以及return


其中,parameter_list是一个用逗号分隔的参数列表,expression是一个返回值的表达式。

例如,以下代码创建了一个lambda表达式,将两个整数相加并返回结果:

Func add = (x, y) => x + y;//其中的Func是泛型委托,它表示一个能够接受两个整数参数并返回一个整数结果的委托
//可以省略参数数据类型,因为编译器根据委托类型推断出参数类型用=>引出来方法体
int result = add(3, 4); // result = 7

扩展方法

什么是扩展方法?

扩展方法,顾名思义,是对原有的对象附加一个新的方法。

可以将扩展方法添加到您自己的自定义类、.NET 框架类或第三方类或接口。

如何区分扩展方法?

【一文详解】知识分享:(C#开发学习快速入门)_第12张图片

如上图,扩展方法在visual studio的intellisense中有一个特殊的符号,这样你就可以轻松区分类方法和扩展方法。


自己如何写扩展方法

在以下示例中,我在 Malema.net 命名空间下创建了一个 IntExtensions 类。

IntExtensions 类中将包含适用于 int 数据类型的所有扩展方法。

(可以为命名空间和类使用任何名称。有时候我们会命名成我们项目当中都要用到的名字,这样就不需在每个类里再引用一次命名空间)

namespace Malema.Extensions
{
    public static class IntExtensions
    {

    }
}

现在,将静态方法定义为扩展方法,其中扩展方法的第一个参数指定扩展方法所适用的类型。 我们将在 int 类型上使用这个扩展方法。 所以第一个参数必须在 int 前面加上 this 修饰符。除了第一个参数,其他参数就是真正的方法形参。

namespace Malema.Extensions
{
    public static class IntExtensions
    {
        public static bool IsGreaterThan(this int i, int value)
        {
            return i > value;
        }
    }
}

现在,可以在要使用此扩展方法的地方 引用 Malema.Extensions 命名空间即可

using System;
using Malema.Extensions;

namespace ConsoleApp{
    class program{
        public static void Main(String[] args){
            var i = 10;
            bool result = i.IsGreaterThan(100);
            Console.WriteLine(result);       
        }
    }
}

写一个泛型扩展方法

namespace Malema.Extensions{
    public static class NumberExtensions{
        public static bool IsGreaterThan(this T i,T value) where T:IComparable{
            return i.CompareTo(value)?false:true;
        }
    }
}

总结:

常规静态方法和扩展方法之间的唯一区别是扩展方法的第一个参数指定要对其进行操作的类型,前面是 this 关键字。


  • 扩展方法是额外的自定义方法,它们最初不包含在类中。
  • 可以将扩展方法添加到自定义、.NET Framework 或第三方类、结构或接口。
  • 扩展方法的第一个参数必须是扩展方法适用的类型,以 this 关键字开头。
  • 通过包含引入扩展方法的命名空间,可以在应用程序的任何地方使用扩展方法。

对象初始化

C#中的对象初始化语法(Object Initializer Syntax)允许你在创建对象的同时对其属性进行初始化,而不止是单靠构造函数,使得代码更为简洁。这种语法通常用于设置对象的属性值,特别是在构造函数的参数列表中没有足够信息来初始化对象时。

在C#中,对象初始化语法通常是通过使用对象初始化器(Object Initializer Syntax)完成的,而不是直接调用构造函数。

这意味着你可以在创建对象的同时初始化其属性,而不必使用括号调用构造函数

1. 基本用法:

// 定义一个类
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// 使用对象初始化语法创建并初始化对象
Person person = new Person
{
    Name = "John Doe",
    Age = 30
};

2. 初始化匿名类型:

对象初始化语法不仅适用于具体的类,还可以用于初始化匿名类型。

var person = new { Name = "John Doe", Age = 30 };

3. 嵌套初始化:

如果类中包含其他对象的引用,也可以使用对象初始化语法为嵌套的对象进行初始化。

public class Address
{
    public string City { get; set; }
    public string Country { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address HomeAddress { get; set; }
}

Person person = new Person
{
    Name = "John Doe",
    Age = 30,
    HomeAddress = new Address
    {
        City = "New York",
        Country = "USA"
    }
};

4. 在集合中使用初始化语法:

在集合的初始化中,你也可以使用对象初始化语法来初始化集合中的对象。

List<Person> people = new List<Person>
{
    new Person { Name = "John Doe", Age = 30 },
    new Person { Name = "Jane Doe", Age = 25 }
};

5. 使用构造函数和初始化语法:

如果类有构造函数,你可以在初始化语法中使用构造函数,并在大括号中为属性赋值。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

Person person = new Person("John Doe", 30);

总的来说,对象初始化语法提供了一种简洁而方便的方式来创建和初始化对象。

注意:

  • 在Java中,没有像C#那样的对象初始化语法,因此对象的初始化主要通过构造函数来完成。

  • 在C#中,对象初始化语句(Object Initialization Syntax)可以使用带括号和不带括号的方式。

    ClassName obj = new ClassName {
        Property1 = value1,
        Property2 = value2,
        // ...
    };
    
    ClassName obj = new ClassName() {
        Property1 = value1,
        Property2 = value2,
        // ...
    };
    

    无论你选择使用括号还是不使用括号,两者都会创建一个新的对象并进行初始化。在实际使用中,括号通常是可选的,除非构造函数本身需要参数。如果构造函数不需要参数,则可以选择省略括号。


继承与多态

  1. 多态的理解: 同一类/类型的多个实例对同一个消息做出不同的反应。

    在平时的应用中,多态性就是指,当使用基类对象指向子类实例对象时,使用这个对象去调用基类和父类中的同名方法

    总的来说,通过基类类型指向派生类对象时,如果方法被重写,将调用派生类中的方法;如果方法没有被重写,将调用基类中的方法。这种行为体现了多态性的概念。

  2. 关于c#中用基类变量声明子类实例对象,在调用基类和子类中的同名方法时的调用结果问题:

    • case1: 当没碰到重写时,不管子类中的同名方法上有没有带有new关键字,用实例对象.同名方法调用的最终都是基类中的同名方法,即方法的隐藏。在这种情况下,派生类的方法会隐藏自己跟基类中的同名方法,使得只会调用基类中的方法。
    • case2: 当碰到重写时,即基类同名方法带有virtual关键字,子类同名方法带有override关键字时,此时用实例对象.同名方法调用的最终都是子类中同名方法
  3. C#中的继承仅支持单一继承。

    继承的语法格式: 当前类名:继承的基类名

  4. 组合优于继承。

    继承的一个坏处继承层次多的时候。如果你想改动底层的一个方法的时候。你得考虑子类的依赖性。

  5. C#中若不想一个类被继承,可以添加sealed关键字。

    【一文详解】知识分享:(C#开发学习快速入门)_第13张图片

  6. C#中所有类型都继承自System.Object(隐式继承,即没有明显的写出xx类 : System.Object的形式).

  7. C#中的base关键字类似于Java中的super关键字。

  8. 子类重写基类方法时,会涉及到virtualoverride关键字。

    其中基类的方法会添加virtual关键字,而子类的同名方法会添加override关键字。

    【而对比Java通过 @Override 注解实现方法重写】


    virtual修饰符:virtual修饰符可以将一个方法声明为虚拟方法。

    即声明当前的方法只是一个占位符,具体的操作都得靠其派生类来实现。

    虚方法的作用在于允许使用多态性,提高灵活性和扩展性; 允许在派生类中覆盖基类的方法,提供定制的实现。

    其中virtual使用的语法格式:

    【一文详解】知识分享:(C#开发学习快速入门)_第14张图片

    其中override使用的语法格式:

    【一文详解】知识分享:(C#开发学习快速入门)_第15张图片

抽象类与接口

抽象类:

  • 方法用abstract关键字修饰,则类也要用abstract关键字修饰。

接口:

  • 接口可以实现多个。
  • 接口名常以大写I开头,如:IMyInterface
  • 接口的实现格式: 类名:接口名

注意:

由于继承和接口实现都是用:的格式,

当碰到某个类既要继承某个类,还有实现某个接口时,此时继承类写在前面,实现的接口写在后面,中间以,隔开:

如: class Manager:Employee,IMyInterface

因为一个类继承的类只能有一个,但实现的接口可以有多个。

抽象类和接口的使用选择:

  • 当涉及到继承/复用时,考虑抽象类,否则优先考虑接口。

C#特性

特性Attribute是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。

其它语言如java中也叫注解 anotation

您可以通过使用特性向程序添加声明性信息。

一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。

特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。

.Net 框架提供了两种类型的特性:预定义特性和自定义特性。

预定义特性

Net 框架提供了三种预定义特性:

  • AttributeUsage
  • Conditional
  • Obsolete

AttributeUsage

预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。

它规定了特性可应用到的项目的类型。

当我们要自己定义Attribute的时候我们就需要用到它了。

规定该特性的语法如下:

[AttributeUsage(validon,AllowMultiple=allowmultiple,Inherited=inherited)]

其中:

  • 参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。
  • 参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
  • 参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。 例如:
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property, 
AllowMultiple = true)]

条件编译 Conditional

这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。

它会引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。

例如,当调试代码时显示变量的值。规定该特性的语法如下:

[Conditional(conditionalSymbol)]

示例:

namespace Malema.net
{
    public class Myclass
    {
        [Conditional("DEBUG")]
        public static void Message(string msg)
        {
            Console.WriteLine(msg);
        }
    }
    class Program
    {
        static async Task Main(string[] args)
        {
            Myclass.Message("hello");
            Console.ReadLine();
        }
    }
}

上面的代码在Debug模式下,会输出 hello。在Release模式下则不会输出hello

【一文详解】知识分享:(C#开发学习快速入门)_第16张图片

Obsolete

这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。

例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法而不是旧方法的消息,来把它标记为 obsolete(过时的)。

规定该特性的语法如下:

[Obsolete(message)]
[Obsolete(message,iserror)]

其中:

  • 参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。
  • 参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。 下面的实例演示了该特性:
[Obsolete("过期了,请使用YourClass.Abc")]
    public class Myclass
    {
        public static void Message(string msg)
        {
            Console.WriteLine(msg);
        }
    }
    class Program
    {
        static async Task Main(string[] args)
        {
            Myclass.Message("helo");
            Console.ReadLine();
        }
    }

会看到如下图这样的编译警告提示:

【一文详解】知识分享:(C#开发学习快速入门)_第17张图片

自定义特性

Net 框架允许创建自定义特性,用于存储声明性的信息,且可在运行时被检索。

创建并使用自定义特性包含三个步骤:

  • 声明自定义特性
  • 在目标程序元素上应用自定义特性
  • 通过反射访问特性 最后一个步骤包含编写一个简单的程序来读取元数据以便查找各种符号。元数据是用于描述其他数据的数据和信息。该程序应使用反射来在运行时访问特性。

声明自定义特性

一个新的自定义特性应派生自 System.Attribute 类。例如:

[AttributeUsage(AttributeTargets.Property)]
public class CsvAttritube:Attribute{
    public string name{get;set;}
    public CsvAttribute(string name){
        this.name = name;
    }
}

应用这个特性

上面我们自定义的CsvAttribute类。但是在使用的时候我们可以把Attribute省略掉。

public class Bar{
    [Csv("OpenPrice")
    public float Open{get;set};
    [Csv("ClosePrice")]
    public float Close{get;set;}
}

如何让这个特性有效果。得靠反射。

C# 反射

反射这个东西 就如同照镜子一样:镜子里面反射出了正在照镜子的人。

反射跟这个的效果就是一样的:通过反射程序我们就可以知道程序是什么样子的。

反射是.Net中获取运行时类型信息的方式。

.Net的应用程序由几个部分:程序集(Assembly)模块(Module)类型(class)组成,而反射提供一种编程的方式,让程序员可以在程序运行期获得这几个组成部分的相关信息.

通过使用反射API

  • 我们可以用反射访问到 程序集 模块 类型 与及类型上面的一些元信息 Attribute, 这些统称为元数据(metadata)。

  • 我们还可以使用反射 动态的创建对象,并对对象的属性字段进行取值和赋值,也可以调用里面的方法包括私有方法

反射优点

  1. 反射提高了程序的灵活性和扩展性。
  2. 降低耦合性,提高自适应能力。
  3. 它允许程序创建和控制任何类的对象,无需提前硬编码目标类。

反射缺点

  1. 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。

    因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。

  2. 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。


反射用到的主要类:

System.Reflection.Assembly类

Assembly 类是 C# 中用于处理程序集(Assembly)的主要类之一。

它提供了获取程序集信息、访问类型和成员、以及执行程序集中的代码的方法。

以下是 Assembly 类的一些常用方法和示例:

1.加载程序集

// 加载当前执行的程序集
Assembly currentAssembly = Assembly.GetExecutingAssembly();

// 加载指定的程序集
Assembly myAssembly = Assembly.LoadFrom("MyAssembly.dll");

2.获取程序集信息:

// 获取当前执行的程序集信息
Assembly currentAssembly = Assembly.GetExecutingAssembly();
Console.WriteLine($"Assembly Name: {currentAssembly.FullName}");

// 获取程序集的类型
Type[] types = currentAssembly.GetTypes();
foreach (Type type in types)
{
    Console.WriteLine($"Type: {type.FullName}");
}

3.获取程序集中的类型和成员:

// 获取当前执行的程序集
Assembly currentAssembly = Assembly.GetExecutingAssembly();

// 获取程序集中的类型
Type myType = currentAssembly.GetType("MyNamespace.MyClass");

// 获取类型中的成员
MethodInfo method = myType.GetMethod("MyMethod");
PropertyInfo property = myType.GetProperty("MyProperty");

4.执行程序集中的代码:

// 获取当前执行的程序集
Assembly currentAssembly = Assembly.GetExecutingAssembly();

// 创建对象实例
object myObject = Activator.CreateInstance(currentAssembly.GetType("MyNamespace.MyClass"));

// 调用方法:Invoke(object obj, object[] parameters);
//obj:表示要在其上调用方法的对象实例;parameters:是一个数组,包含传递给方法的参数
MethodInfo method = currentAssembly.GetType("MyNamespace.MyClass").GetMethod("MyMethod");
method.Invoke(myObject, null);

System.Activator

Activator 类是 C# 中的一个工具类,它提供了在运行时创建对象、调用构造函数以及创建对象实例的功能。Activator 类主要包含一些静态方法,方便在不知道具体类型的情况下动态地创建对象。

以下是 Activator 类的一些常用方法和示例:

1.CreateInstance 方法: 通过指定类型的名称或指定类型的Type 对象,创建该类型的实例

// 通过类型名称创建对象实例
object obj1 = Activator.CreateInstance(Type.GetType("System.String"));

// 通过 Type 对象创建对象实例
Type myType = typeof(int);
object obj2 = Activator.CreateInstance(myType);

2.CreateInstance 泛型方法: 通过指定类型的名称或 指定类型的Type 对象,创建该类型的实例,并进行强类型转换

// 通过类型名称创建对象实例并进行强类型转换
string str = Activator.CreateInstance();

// 通过 Type 对象创建对象实例并进行强类型转换
Type myType = typeof(int);
int num = Activator.CreateInstance();

3.CreateInstanceFrom 方法: 从指定程序集文件中创建对象实例

Assembly myAssembly = Assembly.LoadFrom("MyAssembly.dll");
Type myType = myAssembly.GetType("MyNamespace.MyClass");

object obj = Activator.CreateInstanceFrom("MyAssembly.dll", "MyNamespace.MyClass").Unwrap();

Console.WriteLine(obj.GetType());  // 输出: MyNamespace.MyClass
}

System.Type类:

System.Type 类对于反射起着核心的作用。

它是一个抽象的基类,Type有与每种数据类型对应的派生类,我们使用这个派生类的对象的方法、字段、属性查找有关该类型的所有信息。

获取给定类型的Type引用有3种常用方式:

  • 使用c# typeof运算符

    Type t = typeof(string);
    
  • 使用对象的GetType()方法

    string s = "hello,c#";
    Type t = s.GetType();
    
  • 调用Type类的静态方法GetType()

    Type t = 	Type.GetType(System.String);
    

上面的三种方式都是获取string类型的Type引用对象,在取出string类型的Type引用t之后, 就可以通过t来探测string的类型结构,如下:

string s = "carson";
Type t = s.GetType();

foreach(MememInfo mi in t.GetMembers()){
    Console.WriteLine(mi.Name);
}

Type类的属性:

  • Name 数据类型名

  • FullName 数据类型的完全限定名(包括命名空间名)

  • Namespace 定义数据类型的命名空间名

  • IsAbstract 指示该类型是否是抽象类型

  • IsArray 指示该类型是否是数组

  • IsClass 指示该类型是否是类

  • IsEnum 指示该类型是否是枚举

  • IsInterface 指示该类型是否是接口

  • IsPublic 指示该类型是否是公有的

  • IsSealed 指示该类型是否是密封类

  • IsValueType 指示该类型是否是值类型

Type类的方法:

  • GetConstructor() : GetConstructors()返回ConstructorInfo类型,用于取得该类的构造函数的信息
  • GetEvent(): GetEvents()返回EventInfo类型,用于取得该类的事件的信息
  • GetField(): GetFields()返回FieldInfo类型,用于取得该类的字段(成员变量)的信息
  • GetInterface(): GetInterfaces()返回InterfaceInfo类型,用于取得该类实现的接口的信息
  • GetMember(): GetMembers()返回MemberInfo类型,用于取得该类的所有成员的信息
  • GetMethod(): GetMethods()返回MethodInfo类型,用于取得该类的方法的信息
  • GetProperty(): GetProperties()返回PropertyInfo类型,用于取得该类的属性的信息
  • 可以调用这些成员,其方式是调用Type的InvokeMember()方法,或者调用MethodInfo, PropertyInfo和其他类的Invoke()方法。

反射的具体使用

(1)首先定义一个类用来测试反射的各种常见操作

public class NewClass
    {	
    	//定义各个字段
        public string a;
        public int b;
        public string Name { get; set; }
        public int Age { get; set; }
    	//构造函数1
        public NewClass(string a,int b)
        {
            this.a = a;
            this.b = b;
        }
		//构造函数2
        public NewClass()
        {
            Console.WriteLine("调用构造函数");
        }
		//show方法,显示具体的字段值
        public void show()
        {
            Console.WriteLine("生成一个对象成功,其中a是:" + a +" b是:"+ b +" Name是:"+ this.Name+" Age是:" + this.Age);
        }
    }

(2)利用反射查看类中的构造方法:

//client
{
    //测试类的对象实例化
    NewClass nc = new NewClass();
    //获取Type的引用
    Type t = nc.GetType();
    //获取所有的构造器
    ConstructorInfo[] ci = t.GetConstructors();
    //遍历构造器
    foreach (var c in ci)
    {
        Console.WriteLine("count");
        //获取构造器中的形参信息
        ParameterInfo[] pi = c.GetParameters();
        //遍历形参信息
        foreach (ParameterInfo p in pi)
        {
            //Write输出不换行
            Console.Write("形参类型:" + p.ParameterType.ToString() + " |" + "形参名字:" + p.Name + ",");
        }
        //WriteLine输出换行
        Console.WriteLine();
    }
}

(3)用构造函数动态生成对象

//client
{	
    //获取Type的引用
    Type t = typeof(NewClass);
    //根据对应构造器的形参类型,构造Types数组
    Type[] types = new Type[2];
    types[0] = typeof(string);
    types[1] = typeof(int);
    //传入Types数组,找到对应的构造函数
    ConstructorInfo constructorInfo = t.GetConstructor(types);
    //根据对应构造器对应的实参值,构造object数组
    object[] objs = new object[2] { "5", 6 };
    //传入object数组,调用构造函数生成对象
    object o = constructorInfo.Invoke(objs);
    //对应的对象字段赋值
    ((NewClass)o).Name = "carson";
    ((NewClass)o).Age = 20;
    //调用对应对象的方法
    ((NewClass)o).show();
}

(4)用Activator生成对象**【利用Activator的CreateInstance静态方法**】

Activator 类在反射中使用时,由于是在运行时动态创建对象,因此性能可能不如直接使用构造函数。

在大部分情况下,最好直接使用类型的构造函数来创建对象,而只在必要时才使用 Activator 类的功能

//client
{
    //获取相应类的Type引用
    Type t = typeof(NewClass);
    //法一:构造函数的参数
    object[] objs = new object[2] { "hello", 110 };
    //法一:用Activator的CreateInstance静态方法(调用有参构造方法,传入object数组),生成新对象
    object o = Activator.CreateInstance(t,objs);
    //法二:用Activator的CreateInstance静态方法(调用有参构造方法,不直接传入object数组),生成新对象
    object o = Activator.CreateInstance(t, "hello", 10);

    //法三:用Activator的CreateInstance静态方法(调用无参的构造方法)
    object o = Activator.CreateInstance(t);
    //调用生成对象的方法
    ((NewClass)o).Name = "carson";
    ((NewClass)o).Age = 20;
    ((NewClass)o).show();
}

(5)查看类中的属性(Property)

//获取相应类的Type引用
Type t = typeof(NewClass);
//获取属性
PropertyInfo[] propertyInfos = t.GetProperties();
//遍历输出属性
foreach(PropertyInfo pi in propertyInfos)
{
    Console.WriteLine(pi.Name);
}

(6)查看类中的public方法

//获取相应类的Type引用
Type t = typeof(NewClass);
//获取方法
MethodInfo[] methodInfos = t.GetMethods();
//遍历输出方法
foreach(MethodInfo mi in methodInfos)
{
    Console.WriteLine(mi.ReturnType+" "+mi.Name);
}

(7)查看类中的public字段(Field)

//获取相应类的Type引用
Type t = typeof(NewClass);
//获取Public字段
FieldInfo[] fieldInfos = t.GetFields();
//遍历输出字段
foreach(FieldInfo fi in fieldInfos)
{
    Console.WriteLine(fi.Name);
}

(8)用反射生成对象,并调用属性、方法和字段进行操作

//获取相应类的Type引用
Type t = typeof(NewClass);
//获取Public字段
object obj = Activator.CreateInstance(t);
//取得a字段
FieldInfo aField = t.GetField("a");
//给a字段赋值[传入:实例对象,赋值的具体值]
aField.SetValue(obj, "hello");
//取得b字段
FieldInfo bField = t.GetField("b");
//给a字段赋值
bField.SetValue(obj, 20);

//取得Name属性
PropertyInfo NameProperty = t.GetProperty("Name");
//给Name属性赋值
NameProperty.SetValue(obj, "Carson");

//取得Age属性
PropertyInfo AgeProperty = t.GetProperty("Age");
//给Age属性赋值
AgeProperty.SetValue(obj, 22);

//取得show方法
MethodInfo mi = t.GetMethod("show");
//调用show方法
mi.Invoke(obj, null);

c# 预处理指令

在C#中,预处理指令是用于在代码编译之前执行一些特定操作的指令,它们以#字符开头,不以;结尾。

这些指令用于控制编译过程,并可以在代码中进行条件编译,根据不同的条件选择性地包含或排除代码。

以下是一些常见的C#预处理指令及其示例:

  1. #define#undef

    • #define 用于定义一个符号,可以在代码中用于条件编译。
    • #undef 用于取消已定义的符号。
    #define DEBUG
    using System;
    
    class Program
    {
        static void Main()
        {
    #if DEBUG
            Console.WriteLine("Debug mode is enabled.");
    #else
            Console.WriteLine("Debug mode is not enabled.");
    #endif
    
    #undef DEBUG
    
    #if DEBUG
            Console.WriteLine("This will not be compiled.");
    #else
            Console.WriteLine("Debug mode is not enabled.");
    #endif
        }
    }
    
  2. #if#elif#else

    • #if 用于根据符号是否已定义来包含或排除代码块。
    • #elif 用于在多个条件之间选择一个。
    • #else 用于在没有任何条件匹配时执行。
    #define DEBUG
    #define VERSION1
    
    using System;
    
    class Program
    {
        static void Main()
        {
    #if (DEBUG && !VERSION1)
            Console.WriteLine("Debug mode is enabled.");
    #elif (!DEBUG && VERSION1)
            Console.WriteLine("Version 1 is enabled.");
    #else
            Console.WriteLine("Debug mode is not enabled, and Version 1 is not enabled.");
    #endif
        }
    }
    
  3. #warning#error

    • #warning 用于生成警告消息。
    • #error 用于生成编译错误消息。
    #define VERSION1
    
    using System;
    
    class Program
    {
        static void Main()
        {
    #if !VERSION1
    #warning This code is for a different version.
    #error This code should not be compiled for the current version.
    #endif
    
            Console.WriteLine("Some code here.");
        }
    }
    

这些预处理指令允许您在不同的条件下控制代码的编译行为,这在处理不同配置和环境的代码时非常有用。

请注意,预处理指令在编译时处理,因此编译后的可执行文件将不包含未满足条件的代码块。这有助于优化和定制您的应用程序的构建。

C#结构体

在 C# 中,struct 是表示数据结构的值类型数据类型。它们在栈上分配内存,而不是在堆上,这有助于提高性能。然而,由于是值类型,结构体的拷贝会导致值的复制,因此在某些情况下可能不适用于大型对象。

它可以包含参数化构造函数、静态构造函数、常量、字段、方法、属性、索引器、运算符、事件和嵌套类型。

struct 可用于保存不需要继承的小数据值,例如 坐标点、键值对和复杂的数据结构。

结构体声明

使用 struct 关键字声明结构。 默认访问修饰符是internal

以下示例声明了一个结构 Coordinate。

struct Coordinate
{
    public int x;
    public int y;
}

//使用 new 关键字创建了 Coordinate 结构的对象。 
//它调用结构的默认无参数构造函数,该构造函数将所有成员初始化为其指定数据类型的默认值。
var point = new Coordinate();
Console.WriteLine(point.x); //输出: 0  
Console.WriteLine(point.y); //输出: 0  

//-------------------------------------------
//如果在不使用 new 关键字的情况下声明 struct 类型的变量,则它不会调用任何构造函数,因此所有成员都保持未分配状态。 因此,您必须在访问它们之前为每个成员赋值,否则会产生编译时错误。
struct Coordinate
{
    public int x;
    public int y;
}

Coordinate point;
Console.Write(point.x); // 编译错误

point.x = 10;
point.y = 20;
Console.Write(point.x); //输出: 10  
Console.Write(point.y); //输出: 20  

结构体的构造器

结构不能包含无参数构造函数。 它只能包含参数化构造函数或静态构造函数。

struct Coordinate
{
    public int x;
    public int y;
	
    //必须在参数化构造函数中包含结构体的所有成员,并为它赋值; 否则,如果任何成员未配赋值的话,C# 编译器将给出编译时错误。
    public Coordinate(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    
    public static Coordinate GetOrigin()
    {
        return new Coordinate();
    }
}

Coordinate point = new Coordinate(10, 20);

Console.WriteLine(point.x); //输出: 10  
Console.WriteLine(point.y); //输出: 20  


//---------------
Coordinate point = Coordinate.GetOrigin();

Console.WriteLine(point.x); //输出: 0  
Console.WriteLine(point.y); //输出: 0 

C#枚举

在 C# 中,枚举(Enum)是一种用于定义命名整数常数集合的数据类型。

在 C# 中,枚举 用于将常量名称分配给一组数字整数值。

它使常量值更具可读性,例如,当引用一周中的某一天时,WeekDays.Monday 比数字 0 更具可读性。

以下是一个简单的枚举的定义示例:

public enum DaysOfWeek{
    Sunday,    // 默认值为 0
    Monday,    // 默认值为 1
    Tuesday,   // 默认值为 2
    Wednesday, // 默认值为 3
    Thursday,  // 默认值为 4
    Friday,    // 默认值为 5
    Saturday   // 默认值为 6
}

枚举值

在这个示例中,DaysOfWeek 枚举表示一周中的每一天,每个成员都有一个与其关联的整数值,默认从 0 开始递增。如果你不为枚举成员指定特定的值,它们的值将按照默认规则递增,从 0 开始。

您可以为枚举成员分配不同的值。 枚举成员默认值的更改将自动按顺序将增量值分配给其他成员。

enum Categories
{
    Electronics,    // 0
    Food,           // 1
    Automotive = 6, // 6
    Arts,           // 7
    BeautyCare,     // 8
    Fashion         // 9
}

枚举可以是任何数字数据类型,例如 byte、sbyte、short、ushort、int、uint、long 或 ulong。 但是,枚举不能是字符串类型。

事件

what

当一个对象发生重要事件时,通常需要通知其他对象来执行相应的操作。

事件使对象或类具备通知能力的成员,以手机(对象)举例:手机可以响铃,响铃(事件)使手机具备了通知能力。


事件的作用: 事件是类发送通知或信息到其它类的一种沟通机制C# 中使用事件机制实现线程间的通信


事件是委托的一个子集,为了满足“广播/订阅”模式的需求而生。

为触发事件(To raise an event),需要一个事件发布者,为接收和处理事件,需要一个订阅者或多个订阅者。

这些通常是由发布者类和订阅者类来进行实现。【观察者设计模式】


我们使用事件:

1、解耦我们的应用程序,或者说是松耦合;在没有破坏已有代码的情况下,松耦合的程序容易扩展并达到我们想要做的;

2、用于对象之间联系的运行机制;

3、在不改变已有代码的情况下,提供一种简单而有效的方式去扩展应用程序;

定义

定义一个事件需要使用event关键字,事件依赖于委托,

故其后是一个事件依赖的委托类型(可以是通用委托类型,也可以是自定义的委托类型)。

与定义委托一样,只不过比它多了一个关键字event

// define a delegage(声明一个事件依赖的委托)
public delegate void FoodPreparedEventHandler(object source, EventArgs args);
// declare the event(定义一个事件)
//通常,事件的名称以`On`开头,但也不是必须的。
public event FoodPreparedEventHandler FoodPrepared;

订阅/取订事件

事件以发布-订阅模式进行工作,那意味着一旦我们订阅了事件,只有服务在订阅就在。

但是有时候生意逻辑就规定了可以订阅也可以取消。比如,用户可能有权选择只接收app通知或只接收邮件通知。

我们要能够订阅到我们想要订阅的事件,可以使用+=运算符:

我们要能够取消订阅之前订阅的事件,可以使用-=运算符:

//订阅
orderingService.FoodPrepared += appService.OnFoodPrepared;
orderingService.FoodPrepared += mailService.OnFoodPrepared;
//取消订阅
orderingService.FoodPrepared -= appService.OnFoodPrepared;
orderingService.FoodPrepared -= mailService.OnFoodPrepared;

委托的绑定解绑VS事件的订阅/取订:

  • 委托的绑定和解绑方法可以使用赋值运算符=,也有使用+=-=等运算符。
  • 事件的订阅和取订则只有使用+=-=运算符。

事件运行的底层原理

当不使用event(事件)的应用程序:

以一个食品服务程序为例:

其中的Order类是包含食品类目名称和成分;

其中的FoodOrderingService类是用于处理食品预订的真正服务类;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;


//正如我们看到的,我们拿到了需要准备的菜单,模拟4秒钟的等待时间以进行准备,然后我们发送了一条通知到用户程序:预订在准备。
//当然,这仅仅是个测试所以示例相当简单,我们用console模拟通知消息。在实际的应用中会包含很多步骤。
namespace Order
{
    class FoodOrderingService
    {	//调用服务来购买一个带很多奶酪的pizza:
        static void Main(string[] args)
        {
            var order = new Order { Item = "Pizza with extra cheese" };
            var orderingService = new FoodOrderingService();
            orderingService.PrepareOrder(order);
        }
		
        //食品准备服务
        public void PrepareOrder(Order order)
        {
            Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item);
            Thread.Sleep(4000);

            AppService _appService = new AppService();
            _appService.SendAppNotification();
        }
    }
    //发送通知的服务类
    public class AppService
    {
        public void SendAppNotification()
        {
            Console.WriteLine("AppService:your food is prepared!");
        }
    }
	//食品订单类
    public class Order
    {
        public string Item { get; set; }
        public string Ingredients{get;set;}
    }
}

但是,我们决定扩展程序用email通知用户他们的订餐准备好了。

为达到上面的目的,需扩展service类代码并修改相关代码,结果如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Order
{
    class FoodOrderingService
    {
        static void Main(string[] args)
        {
            var order = new Order { Item = "Pizza with extra cheese"};
            var orderingService = new FoodOrderingService();
            orderingService.PrepareOrder(order);
            Console.ReadKey();
        }

        public void PrepareOrder(Order order)
        {
            Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item);
            Thread.Sleep(4000);

            AppService _appService = new AppService();
            _appService.SendAppNotification();

            MailService _mailService = new MailService();
            _mailService.SendEmailNotification();
        }
    }
    
    public class AppService
    {
        public void SendAppNotification()
        {
            Console.WriteLine("AppService:your food is prepared!");
        }
    }
    
    public class MailService
    {
        public void SendEmailNotification()
        {
            Console.WriteLine("MailService:your food is prepared.");
        }
    }
	
    public class Order
    {
        public string Item { get; set; }
        public string Ingredients{get;set;}
    }

}

这很容易给我们的程序引入bug,甚至是如果我们写了个单元测试,我们可能要重新梳理并修改代码。

同理,我们在FoodOrderingService类中同时引入appservice 和 mailservice函数来发送通知消息,创建了一个紧密相联的耦合程序。

这种实现方法不是我们期望的!我们尝试使用event事件提升一下这个示例,这是引入发布-订阅模式的完美场合.


当使用了event(事件)的应用程序:

看一下如何运用委托和事件用上面的示例来演示event事件

创建事件发布者。对于事件发布者,我们需要做:

1、定义一个委托;

2、定义一个依赖委托的事件;

3、触发事件;


1.定义一个事件依赖的委托

  • 事件中定义的委托通常有2个参数:

    第一个参数是事件源(表示触发事件的那个组件 如(button/label/listview…),比如说你单击button,那么sender就是button),更确切地说是将要发布事件的那个类;

    第二个参数EventArgs(它用来辅助你处理事件,比如说你用鼠标点击窗体,那么EventArgs是会包含点击的位置等等),是与事件相关的任何其它数据。

  • 通常来说,我们会给委托起一个描述性的名字,比如“FoodPrepared”,然后在名字末尾添加上“EventHandler”。

    无论委托名称怎么变化,人们都能很容易了解这是个委托。

public class FoodOrderingService
{
    // define a delegate(声明一个委托)
    //委托FoodPreparedEventHandler返回void
    //事件是FoodPreparedEventHandler类型,因为我们定义的是一旦操作完成就会触发的事件,所以给它起了个过去时的名字---FoodPrepared。
    public delegate void FoodPreparedEventHandler(object source, EventArgs args);

    ...
}

2.再定义一个依赖委托的事件

定义一个事件需要使用event关键字,其后是一个委托类型(可以是通用委托类型,也可以是自定义的委托类型)。

与定义委托一样,只不过比它多了一个关键字event。

public class FoodOrderingService
{
    // define a delegage(声明一个事件依赖的委托)
    public delegate void FoodPreparedEventHandler(object source, EventArgs args);
    // declare the event(定义一个事件)
    //通常,事件的名称以`On`开头,但也不是必须的。
    public event FoodPreparedEventHandler FoodPrepared;

    ...
}

3.触发事件,我们再创建一个用于触发事件的方法函数

public class FoodOrderingService
{	//delegate
    public delegate void FoodPreparedEventHandler(object source, EventArgs args);
    //event
    public event FoodPreparedEventHandler FoodPrepared;
	//被调用的函数
    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);
		//调用触发事件的方法函数
        OnFoodPrepared();
    }
	//触发事件的方法函数	
    //按照惯例,方法函数的修饰符应该是protected virtual void,名称前缀加“On”。
    protected virtual void OnFoodPrepared()
    {	
        //触发事件(调用委托(调用委托绑定的具体方法))
        //函数体内部,我们检查是否有订阅者(FoodPrepared != null),如果有订阅者,我们就调用事件,将this做为参数传递,this是当前类;null做为事件参数。
        if (FoodPrepared != null)
            FoodPrepared(this, null);
    }
}

创建订阅者:

1.创建AppService类和MailService

//订阅者1
public class AppService
{
    //其中的事件处理方法
    public void OnFoodPrepared(object source, EventArgs eventArgs)
    {
        Console.WriteLine("AppService: your food is prepared.");
    }
}
//订阅者2
public class MailService
{
    //其中的事件处理方法
    public void OnFoodPrepared(object source, EventArgs eventArgs)
    {
        Console.WriteLine("MailService: your food is prepared.");
    }
}

2.现在实例化AppService类MailService类,并订阅到FoodPrepared事件。

用 += 运算符可以订阅到事件,在示例中我们将订阅者AppService类和订阅者MailService类订阅到FoodPrepared事件中,用AppService类中的OnFoodPrepared函数以及MailService类中的OnFoodPrepared函数来处理事件。

static void Main(string[] args)
{
    //订单对象初始化
    var order = new Order { Item = "Pizza with extra cheese" };
	//发布者
    var orderingService = new FoodOrderingService();
    //订阅者
    var appService = new AppService();
    var mailService = new MailService();
	//订阅事件
    orderingService.FoodPrepared += appService.OnFoodPrepared;
    orderingService.FoodPrepared += mailService.OnFoodPrepared;
	//调用对应方法,其中触发了事件
    orderingService.PrepareOrder(order);
	
    Console.ReadKey();
}

3.故完整代码如下:

1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading;
 6 
 7 namespace Order
 8 {
       //client
 9     class Program
10     {
11         static void Main(string[] args)
12         {
13             var order = new Order { Item = "Pizza with extra cheese" };
14             var orderingService = new FoodOrderingService();
15             var appService = new AppService();
16             var mailService = new MailService();
17 
18             orderingService.FoodPrepared += appService.OnFoodPrepared;
19             orderingService.FoodPrepared += mailService.OnFoodPrepared;
20 
21             orderingService.PrepareOrder(order);
22             Console.ReadKey();
23         } 
24     }
25 		//发布者
26     class FoodOrderingService
27     {
28         //定义委托
29         public delegate void FoodPreparedEventHandler(object source, EventArgs args);
30         //声明事件
31         public event FoodPreparedEventHandler FoodPrepared;
32 
33         public void PrepareOrder(Order order)
34         {
35 
36             Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item);
37             Thread.Sleep(4000);
38 				//调用触发事件的方法
39             OnFoodPrepared();
40         }
41         //触发事件的函数方法
42         protected virtual void OnFoodPrepared()
43         {
44             if (FoodPrepared != null)
45             {
46                 FoodPrepared(this,null);
47             }
48         }
49     }
       //订阅者
50     public class AppService
51     {
52         public void OnFoodPrepared(object source ,EventArgs eventArgs)
53         {
54             Console.WriteLine("AppService:your food is prepared!");
55         }
56     }
     	//订阅者
57     public class MailService
58     {
59         public void OnFoodPrepared(object sorece , EventArgs eventArgs)
60         {
61             Console.WriteLine("MailService:your food is prepared.");
62         }
63     }
64 	   //订单类
65     public class Order
66     {
67         public string Item { get; set; }
68         public string Ingredients{get;set;}
69     }
70 
71     
72 }

可以像这样无限地扩展我们的程序,我们也可以将FoodOrderingService类移到其它类库中或我们想要的地方。

扩展EventArgs参数

像前面提到的那样,之前我们用EventArgs发送事件数据。

但是其实我们能创建自EventArgs继承的自定义事件参数类:FoodPreparedEventArgs,自定义格式内容,从而用于发送数据到订阅者。

//扩展和继承自EventArgs的自定义事件参数类:FoodPreparedEventArgs
1 public class FoodPreparedEventArgs : EventArgs
2 {
     //自定义字段,其是发送给订阅者的数据
3     public Order Order { get; set; }
4 }

然后,我们修改发布者FoodOrderingService中的事件参数类型:

//现在我们正发送order数据(OnFoodPrepared(order);)到订阅者。
//用FoodPreparedEventArgs代替了EventArgs并将order信息传递给了订阅者。
public class FoodOrderingService
{
    //事件参数替换为:FoodPreparedEventArgs
    public delegate void FoodPreparedEventHandler(object source, FoodPreparedEventArgs args);
    public event FoodPreparedEventHandler FoodPrepared;

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        OnFoodPrepared(order);
    }

    protected virtual void OnFoodPrepared(Order order)
    {
        if (FoodPrepared != null)
            FoodPrepared(this, new FoodPreparedEventArgs { Order = order });
    }
}

修改订阅者中的事件参数类型:

//AppService类
public class AppService
{
    public void OnFoodPrepared(object source ,FoodPreparedEventArgs eventArgs)
    {
         Console.WriteLine($"AppService: your food '{eventArgs.Order.Item}' is prepared.");
    }
}
    
//MailService类
public class MailService
{
    public void OnFoodPrepared(object sorece , FoodPreparedEventArgs eventArgs)
    {
        Console.WriteLine($"MailService: your food '{eventArgs.Order.Item}' is prepared.");
    }
}

完整代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Order
{
    class Program
    {
        static void Main(string[] args)
        {
            var order = new Order { Item = "Pizza with extra cheese" };
            var orderingService = new FoodOrderingService();
            var appService = new AppService();
            var mailService = new MailService();

            orderingService.FoodPrepared += appService.OnFoodPrepared;
            orderingService.FoodPrepared += mailService.OnFoodPrepared;

            orderingService.PrepareOrder(order);
            Console.ReadKey();
        }
    }

    class FoodOrderingService
    {
        //定义委托
        public delegate void FoodPreparedEventHandler(object source, FoodPreparedEventArgs args);
        //声明事件
        public event FoodPreparedEventHandler FoodPrepared;

        public void PrepareOrder(Order order)
        {

            Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item);
            Thread.Sleep(4000);
			//触发事件
            OnFoodPrepared(order);
        }
        //触发事件的函数方法
        protected virtual void OnFoodPrepared(Order order)
        {
            if (FoodPrepared != null)
            {
                FoodPrepared(this, new FoodPreparedEventArgs { Order = order});
            }
        }
    }
    public class AppService
    {
        public void OnFoodPrepared(object source, FoodPreparedEventArgs eventArgs)
        {
            Console.WriteLine("AppService:your food {0} is prepared!",eventArgs.Order.Item);
        }
    }
    public class MailService
    {
        public void OnFoodPrepared(object sorece , FoodPreparedEventArgs eventArgs)
        {
            Console.WriteLine("MailService:your food {0} is prepared!", eventArgs.Order.Item);
        }
    }

    public class Order
    {
        public string Item { get; set; }
        public string Ingredients{get;set;}
    }
	//自定义的事件参数类
    public class FoodPreparedEventArgs : EventArgs
    {
        public Order Order { get; set; }
    }
}

用EventHandler类来简化事件使用

现在,通过**.net**用一些适合我们的现代方式修改代码,在C#中使用event handler。

  • 主要使用EventHandlerEventHandler创建事件,他们是专门的封装器,可以简化事件的创建。

EventHandler类似于提供了的已经封装好了的事件依赖的委托类型。

EventHandler类<>的TEventArgs代表自定义的事件相关参数类型。

  • 使用了传统的Invoke()函数来触发事件,避免复杂的null检查,可以清理项目冗余代码。

所以现在我们通过添加event handler修改上面的FoodOrderingService类:

public class FoodOrderingService
{
    //使用EventHandler类型创建事件(委托)
    //现在,用于代替委托和事件的两种声明,我们用EventHandler和FoodPreparedEventArgs,这样可以使代码更简洁,更有可读性。      //这可能是你在其它项目中使用事件时会看到的。
    public event EventHandler FoodPrepared;

    public void PrepareOrder(Order order)
    {
        Console.WriteLine($"Preparing your order '{order.Item}', please wait...");
        Thread.Sleep(4000);

        OnFoodPrepared(order);
    }
	
    protected virtual void OnFoodPrepared(Order order)
    {
        //使用了传统的Invoke()函数来触发事件,避免复杂的null检查,可以清理项目冗余代码。
        FoodPrepared?.Invoke(this, new FoodPreparedEventArgs { Order = order });
    }
}

c#流(stream)

C# 包括以下标准 IO(输入/输出)类,用于从文件、内存、网络、隔离存储等不同来源读取/写入。

字节流

【一文详解】知识分享:(C#开发学习快速入门)_第18张图片

Stream:System.IO.Stream 是一个抽象类,它提供了将字节(读取、写入等)传输到源的标准方法。它就像一个包装类来传输字节。需要从特定源读取/写入字节的类必须实现 Stream 类。


以下字节类继承 Stream 类以提供从特定源读取/写入字节的功能:

FileStream: 从/向物理文件读取或写入字节,无论它是 .txt、.exe、.jpg 还是任何其他文件。 FileStream 派生自 Stream 类。

MemoryStream:MemoryStream 读取或写入存储在内存中的字节。

BufferedStream:BufferedStream 从其他流读取或写入字节以提高某些 I/O 操作的性能。

NetworkStream:NetworkStream 从网络套接字读取或写入字节。

PipeStream:PipeStream 从不同进程读取或写入字节。

CryptoStream:CryptoStream 用于将数据流链接到加密转换。


字符流

StreamReader:StreamReader 是一个辅助类,用于通过使用编码值将字节转换为字符来从 Stream 中读取字符。 它可用于从不同的流(如 FileStream、MemoryStream 等)读取字符串(字符)。

StreamWriter:StreamWriter 是一个帮助类,用于通过将字符转换为字节来将字符串写入 Stream。 它可用于将字符串写入不同的流,例如 FileStream、MemoryStream 等。

BinaryReader:BinaryReader 是一个帮助类,用于从字节读取原始数据类型。

BinaryWriter:BinaryWriter 以二进制形式写入原始类型。

举个例子:
【一文详解】知识分享:(C#开发学习快速入门)_第19张图片

上图显示 FileStream 从物理文件中读取字节,然后 StreamReader 通过将这些字节转换为字符串来读取字符串。 同理,StreamWriter 将字符串转换为字节写入 FileStream,然后 FileStream 将字节写入物理文件。

因此,FileStream 处理字节,而 StreamReader 和 StreamWriter 处理字符串。


C# 文件操作

C# 提供了下面的这些类用来操作文件,它们可以用来访问文件夹,文件,打开文件,写入文件,创建文件, 移动文件,列出目录下所有的文件等等。

类名 用法
File File 是一个静态类,提供各种功能,如复制、创建、移动、删除、打开以读取或/写入、加密或解密、检查文件是否存在、添加新的文件内容、获取最后访问权限 时间等
FileInfo FileInfo 类提供与静态 File 类相同的功能。 通过手动编写代码来读取或写入文件中的字节,您可以更好地控制如何对文件进行读/写操作。
Directory Directory 是一个静态类,提供创建、移动、删除和访问子目录的功能。
DirectoryInfo DirectoryInfo 提供了创建、移动、删除和访问子目录的实例方法。
Path Path 是一个静态类,它提供检索文件扩展名、更改文件扩展名、检索绝对物理路径以及其他与路径相关的功能等功能。

File类

静态 File 类包括各种实用方法来与任何类型的物理文件进行交互,例如二进制,文本等

使用这个静态 File 类对物理文件执行一些快速操作。

File是在System.IO下面的,所以我们得引用这个命名空间:

using System.IO;

File 的重要方法:

方法 用法
AppendAllLines 在文件中追加行,然后关闭文件。如果指定的文件不存在,此方法会创建一个文件,将指定的行写入文件,然后关闭文件。
AppendAllText 打开文件,将指定的字符串附加到文件,然后关闭文件。如果文件不存在,此方法会创建一个文件,将指定的字符串写入文件,然后关闭文件。
AppendText 创建一个 StreamWriter,它将 UTF-8 编码的文本附加到现有文件,如果指定的文件不存在,则附加到新文件。
Copy 将现有文件复制到新文件。不允许覆盖同名文件。
Create 创建或覆盖指定路径中的文件。 返回一个FileStream
CreateText 创建或打开用于写入 UTF-8 编码文本的文件。 返回一个StreamWriter
Decrypt 解密当前帐户使用加密方法加密的文件。 (依赖于CSP)
Delete 删除指定的文件。
Encrypt 加密文件,以便只有用于加密文件的帐户才能对其进行解密。 (依赖于CSP)
Exists 确定指定的文件是否存在。
Move 将指定文件移动到新位置,提供指定新文件名的选项。
Open 在指定路径上打开一个具有读/写访问权限的 FileStream。
ReadAllBytes 打开二进制文件,将文件内容读入字节数组,然后关闭文件。
ReadAllLines 打开一个文本文件,读取文件的所有行,然后关闭文件。
ReadAllText 打开一个文本文件,读取文件的所有行,然后关闭文件。
Replace 用另一个文件的内容替换指定文件的内容,删除原始文件,并创建替换文件的备份。
WriteAllBytes 创建一个新文件,将指定的字节数组写入文件,然后关闭文件。如果目标文件已经存在,它会被覆盖。
WriteAllLines 创建一个新文件,将一组字符串写入该文件,然后关闭该文件。
WriteAllText 创建一个新文件,将指定的字符串写入文件,然后关闭文件。如果目标文件已经存在,它会被覆盖。

FileInfo类

FileInfo 类提供与静态 File 类相同的功能,但您可以通过手动编写代码来从文件读取或写入字节,从而更好地控制文件的读/写操作。

其重要属性和方法:

属性 说明
Directory 获取父目录的实例。
DirectoryName 获取表示目录完整路径的字符串。
Exists 获取一个值,该值指示文件是否存在。
Extension 获取表示文件扩展名部分的字符串。
FullName 获取目录或文件的完整路径。
IsReadOnly 获取或设置一个值,该值确定当前文件是否为只读。
LastAccessTime 获取或设置上次访问当前文件或目录的时间
LastWriteTime 获取或设置当前文件或目录上次写入的时间
Length 获取当前文件的大小(以字节为单位)。
Name 获取文件的名称。
方法 说明
AppendText 创建一个 StreamWriter,它将文本附加到由 FileInfo 的此实例表示的文件。
CopyTo 将现有文件复制到新文件,不允许覆盖现有文件。
Create 创建文件。 返回 FileStream
CreateText 创建一个写入新文本文件的 StreamWriter。
Decrypt 解密当前帐户使用加密方法加密的文件。 (依赖于CSP)
Delete 删除指定的文件。
Encrypt 加密文件,以便只有用于加密文件的帐户才能对其进行解密。 (依赖于CSP)
MoveTo 将指定文件移动到新位置,提供指定新文件名的选项。
Open 在指定的 FileMode 中打开一个。
OpenRead 创建一个只读的 FileStream。
OpenText 使用 UTF8 编码创建一个从现有文本文件读取的 StreamReader。
OpenWrite 创建一个只写的 FileStream。
Replace 用当前 FileInfo 对象描述的文件替换指定文件的内容,删除原始文件,并创建替换文件的备份。
ToString 以字符串形式返回路径。

Directory类

操作文件夹的一个静态类。

下面是其常用的一些方法:

说明
CreateDirectory(String) 在指定路径中创建所有目录和子目录,除非它们已经存在。
Delete(String) 从指定路径删除空目录。
Delete(String, Boolean) 删除指定的目录,并删除该目录中的所有子目录和文件(如果表示)。
Exists(String) 确定给定路径是否引用磁盘上的现有目录。
GetCurrentDirectory() 获取应用程序的当前工作目录。
GetDirectories(String) 返回指定目录中的子目录的名称(包括其路径)。
GetDirectories(String, String) 返回指定目录中与指定的搜索模式匹配的子目录的名称(包括其路径)。
GetDirectories(String, String, EnumerationOptions) 返回指定目录中与指定的搜索模式和枚举选项匹配的子目录的名称(包括其路径)。
GetDirectories(String, String, SearchOption) 返回与在指定目录中的指定搜索模式匹配的子目录的名称(包括其路径),还可以选择地搜索子目录。
GetFiles(String) 返回指定目录中文件的名称(包括其路径)
GetFiles(String, String) 返回指定目录中与指定的搜索模式匹配的文件的名称(包含其路径)。
GetFiles(String, String, EnumerationOptions) 返回指定目录中与指定的搜索模式和枚举选项匹配的文件的名称(包括其路径)
GetFiles(String, String, SearchOption) 返回指定目录中与指定的搜索模式匹配的文件的名称(包含其路径),使用某个值确定是否要搜索子目录。
GetFileSystemEntries(String) 返回指定路径中的所有文件和子目录的名称。
GetFileSystemEntries(String, String) 返回一个数组,其中包含与指定路径中的搜索模式相匹配的文件名和目录名称。
GetFileSystemEntries(String, String, EnumerationOptions) 返回指定路径中与搜索模式和枚举选项匹配的文件名和目录名的数组。
GetFileSystemEntries(String, String, SearchOption) 返回指定路径中与搜索模式匹配的所有文件名和目录名的数组,还可以搜索子目录。
Move(String, String) 将文件或目录及其内容移到新位置。

DirectoryInfo类

主要用来获取当前的目录名字为主,因为其跟Directoy都是类似的

class Program
{
    static async Task Main(string[] args)
    {
        var info = new DirectoryInfo(@"D:\GitHub\Articles\csharp");
        Console.WriteLine(info.Name);
    }
}

Path类

合并路径

static async Task Main(string[] args)
{
    var path = Path.Combine("abc", "dd");
    Console.WriteLine(path);
}
//windows下 输出 abc\dd linux 下输出的是 abc/dd

得到扩展名

static async Task Main(string[] args)
{
    var extension = Path.GetExtension("asdf/sdfsdf/sdf.png");
    Console.WriteLine(extension);
}

得到文件名

static async Task Main(string[] args)
{
    var fileName = Path.GetFileName("d:/as/aa/abc.txt");
    Console.WriteLine(fileName);//abc.txt

    var fileNameWithoutExtension = Path.GetFileNameWithoutExtension("d:/as/aa/abc.txt");
    Console.WriteLine(fileNameWithoutExtension);//abc
}

创建临时文件

static async Task Main(string[] args)
{
    var tempFile = Path.GetTempFileName();
    Console.WriteLine(tempFile);
}

异步编程

异步与多线程的关系

异步是一种编程模型,目的是在等待 I/O 操作时不阻塞调用io操作的线程,以提高程序的效率和响应性。

多线程是一种并发编程模型,通过同时运行多个线程来提高性能,但需要注意线程同步和共享资源的问题,避免竞态条件和死锁。

综合考虑:

  • 在 I/O 密集型任务中,异步编程通常更适用,因为它能够更有效地利用资源,提高响应性。
  • 在 CPU 密集型任务中,多线程可能更合适,因为它可以在多个核上并行执行任务,提高计算性能。

总的来说,选择异步还是多线程取决于具体的应用场景和任务类型。在现代编程中,往往会将异步编程和多线程结合使用,以充分发挥它们各自的优势。

Task类

概述:

  • Task 是用于表示一个异步操作的类,通常用于没有返回值的异步方法。
  • 可以通过 Task.RunTask.Factory.StartNew 创建 Task 对象,也可以在异步方法中直接返回 Task。
  • 选择异步方法的返回类型取决于你的需求和应用场景。通常情况下,推荐使用 TaskTask,因为它们提供了一种通用的方式来表示异步操作,并且能够充分利用异步编程的优势。

创建 Task 对象:

// 使用 Task.Run,通常情况下,推荐使用 Task.Run,因为它提供了一种简洁且性能良好的方式来在新的任务上执行操作。
Task task1 = Task.Run(() => SomeMethod());

// 使用 Task.Factory.StartNew
Task task2 = Task.Factory.StartNew(() => SomeMethod(),
                                    CancellationToken.None,
                                    TaskCreationOptions.DenyChildAttach,
                                    TaskScheduler.Default);

创建Task对象时Task.Run 和 Task.Factory.StartNew 之间的区别是什么?

  1. 默认调度器
    • Task.Run 使用 TaskScheduler.Default,它是线程池调度器,通常用于执行计算密集型的任务。
    • Task.Factory.StartNew 默认使用 TaskScheduler.Current,它继承自调用线程的任务调度器,适用于 UI 线程或其他自定义调度器。
  2. 异常处理
    • Task.Run 更加友好地处理异常,不会封装在 AggregateException 中。
    • Task.Factory.StartNew 在没有明确指定选项的情况下,可能会将异常封装在 AggregateException 中。
  3. 默认选项
    • Task.Run 使用一组默认选项,适用于大多数常见的情况。
    • Task.Factory.StartNew 需要手动配置选项,例如 TaskCreationOptionsTaskScheduler

异步方法返回 Task:

public async Task DoAsyncOperation()
{
    // 异步操作
}

等待 Task 完成:

Task myTask = DoAsyncOperation();
await myTask;

使用 Task.ContinueWith 处理完成后的任务:

Task myTask = Task.Run(() => SomeMethod());
myTask.ContinueWith(task =>  Console.WriteLine("Task Completed"));

Task返回异常信息

在异步方法中,如果 Task 返回了异常,可以通过检查 Task.Exception 属性来获取异常信息。

Task myTask = Task.Run(() => SomeMethod());
Console.WriteLine(myTask.Exception);

Task

概述:

  • TaskTask 的泛型版本,用于表示一个异步操作,其中包含返回值。
  • 通过 Task.RunTask.Factory.StartNew 创建 Task 对象,或者在异步方法中直接返回 Task

创建 Task 对象:

Task myTask = Task.Run(() => SomeMethodReturningInt());

异步方法返回 Task:

public async Task GetNumberAsync()
{
    // 异步操作,并返回一个整数结果
    return await SomeAsyncNumberOperation();
}

等待 Task 完成并获取结果:

Task myTask = GetNumberAsync();
int result = await myTask;

async与await关键字

在async修饰的方法中,总会出现await的身影。所以你想抛开async和await中的某一个,去单独认识另一个是很难的。

async概述

async是一个关键字,同时也是修饰符(和abstract、static一样)。
使用async修饰符可以将一个方法、lambda表达式或匿名方法指定为异步的。

如果在方法或表达式上使用async修饰符,则它就被称为异步方法(async method)。

下面示例代码定义了一个名为ExampleMethodAsync的异步方法:

public async Task ExampleMethodAsync(){
    //...
}

异步方法的返回类型:

具体取决于异步操作是否有返回值:

  • 异步方法的返回类型可以是 Task,表示异步操作完成,没有返回值。
  • 也可以是 Task,表示异步操作完成,并返回一个类型为 TResult 的结果。

await概述

await是一个operator(运算符或者操作符)。

该运算符会挂起(suspend)封闭的异步方法(async method),直到操作对象的异步操作完成。

这句话初看比较难懂,稍微拆解一下。

  • operator,运算符,跟加减乘除一样,作用于某个值(或对象),然后该值会进行一些运算,发生变化。
  • 挂起,就是使某个过程暂停。
  • 封闭的,我们可以想象方法(或者说函数)是一个容器,里面装载了一些运算的语句,随着运算的进行,方法(容器)中的状态会发生变化,此时我挂起方法,就相当于把方法(连同那些状态)封闭起来,不再改变。
  • 异步方法,指的是该方法不是阻塞的,我运行到某个点,可能要等很久,此时我不等了,直接去干别的事情了,该点运行完之后通知我回来继续运行。
  • 直到操作对象的异步操作完成,就是说await作用的对象的其他异步操作还在进行,进行完了我再回来继续执行await下面的语句。

在异步编程中,“挂起” 通常指的是在异步操作中暂停当前方法的执行,以等待某些异步操作的完成。

await 表示在执行异步操作时,将控制权返回给调用方,直到异步操作完成后再继续执行。

//下面是一个简单的示例,用于理解方法的挂起:
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("Before DoAsync");

        // 调用异步方法 DoAsync
        await DoAsync();

        Console.WriteLine("After DoAsync");
    }

    static async Task DoAsync()
    {
        Console.WriteLine("Start DoAsync");

        // 模拟一个异步操作,比如网络请求或I/O操作
        await Task.Delay(2000);

        Console.WriteLine("End DoAsync");
    }
}

//在这个例子中,Main 方法调用了异步方法 DoAsync。在 DoAsync 方法内部,使用了 await Task.Delay(2000) 来模拟一个异步操作,此时控制权会在异步操作执行期间返回给调用方(Main 方法),这时 Main 方法的执行并没有被阻塞,而是可以继续执行其他操作。当异步操作完成后,DoAsync 方法会在 await 处恢复执行,之后的代码才会执行。

//总体来说,异步方法的 "挂起" 是指在遇到异步操作时,暂停当前方法的执行,将控制权返回给调用方,等待异步操作完成后再继续执行。这使得应用程序在执行异步操作的同时可以继续执行其他任务,提高了程序的响应性。

画个草图,理解await运算符

蓝色手描实线表示该方法正在执行。
异步≠多线程,但异步往往会和多线程一起用。

【一文详解】知识分享:(C#开发学习快速入门)_第20张图片

  • 只能在async关键字修饰的方法、lambda表达式或匿名方法上使用await运算符。

  • await 关键字用于等待异步操作的完成。它会暂时将控制权返回给调用方,允许调用方在等待的过程中执行其他操作。

  • 当遇到 await 表达式时,当前方法会在此处分割,将剩余部分作为回调(或称为后续任务)注册到异步操作的完成事件上。


async代码示例:

在对await有个大概理解后,继续学习async关键字

假设下面代码是在一个异步方法中的,并且它调用了 HttpClient.GetStringAsync方法:

string contents = await httpClient.GetStringAsync(requestUrl);

异步方法会以同步的方式运行,直到遇到await表达式,此时该方法被挂起(await表达式下面的语句停止执行),直到等待的任务完成。

与此同时,控制(执行)权返回给方法的调用者。

如果async关键字修饰的方法不包含await表达式或语句,则该方法将同步执行。编译器会警告你该异步方法不包含await语句,因为这种情况可能会指示错误。


总结一下:

1️⃣async和await往往一起出现.

2️⃣async修饰符,指明该方法是一个异步方法,异步方法应该使用 asyncawait 关键字。这样可以使异步代码更加清晰。

3️⃣await运算符,等候操作对象的异步操作完成


正确使用 async 和 await 的步骤:

  1. 方法声明: 使用 async 关键字来修饰异步方法。
  2. 返回类型: 异步方法的返回类型应该是 TaskTask,取决于是否有返回值。
  3. await 关键字: 在异步方法中使用 await 关键字等待异步操作的完成。
  4. 异常处理: 使用 try-catch 块捕获异步方法中可能抛出的异常。

异步操作

what is 异步操作?

异步操作是一种允许程序继续执行其他任务而不必等待某个长时间运行的操作完成的编程模型。在异步操作中,程序可以启动一个任务,然后继续执行其他任务,而无需等待启动的任务完成。这对于执行可能涉及网络请求、文件 I/O、长时间计算等的操作特别有用,因为这些操作可能需要一定的时间来完成。

异步操作的主要优势在于提高程序的响应性和效率,特别是在处理用户界面(UI)或执行多个操作的情况下。而在同步操作中,如果一个操作耗时较长,程序可能会在等待这个操作完成时变得不响应。

在C#中,异步操作通常使用 asyncawait 关键字,这使得异步编程变得更加清晰和易于理解。

更加具体的来说,异步操作指的是的async异步方法中加了await关键字的相关表达式

如何取消异步操作
取消异步操作通常使用 CancellationTokenCancellationTokenSource 来实现。

1.使用CancellationToken参数:

首先,在异步方法中使用 CancellationToken 参数,例如:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Test{
    public class AsyncCancellationExample{
        public async Task SomeAsyncOperation(CancellartionToken cancellationToken){
            Console.WriteLine("Async operation started.");
            try{
                //模拟异步操作
                await Task.Delay(5000,cancellationToken);
                
                //在异步操作中检查CancellationToken是否已取消
                cancellationToken.ThrowIfCancellationRequested();
                
                Console.WriteLine("Async operation completed.");
            }catch(OperationCanceledException){
                 Console.WriteLine("Async operation canceled.");
            }
            
        }
    }
    
    public void CancelAsyncOperation(){
        //在需要取消异步操作的地方调用Cancel方法
        cts?.Cancel();
    }
}

2.使用CancellationTokenSource参数:

在调用异步方法的地方,创建一个 CancellationTokenSource 对象,并将其 Token 属性传递给异步方法。

在需要取消异步操作的地方,调用 CancellationTokenSourceCancel 方法。

在这里,StartAsyncOperation 方法中使用 using 语句创建一个新的 CancellationTokenSource 对象,确保每次开始新的异步操作时都使用一个新的 CancellationTokenSource

using System;
using System.Threading;
using System.Threading.Tasks;

public class AsyncCancellationExample
{
    private CancellationTokenSource cts;
    
    public async Task StartAsyncOperation(){
        try{
            // 创建一个新的异步操作的 CancellationTokenSource
            using(cts = new CancellationTokenSource()){
                //调用异步方法,并传递CancellationTokenSource的Token属性给异步方法
                await SomeAsyncOperation(cts.Token);
            }
            Console.WriteLine("Async operation completed successfully.");
        }catch (OperationCanceledException)
        {
            Console.WriteLine("Async operation canceled.");
        }
    }
}

c# 异常

【一文详解】知识分享:(C#开发学习快速入门)_第21张图片

Exception 类是 SystemException 和 ApplicationException 类的基类。

SystemException 类是程序执行过程中可能发生的所有异常的基类。

如果我们程序希望定义异常的话可以继承于Exception.

下面列出了一些派生自 System.SystemException 类的预定义的异常类:

异常类 描述
System.IO.IOException 处理 I/O 错误。
System.IndexOutOfRangeException 处理当方法指向超出范围的数组索引时生成的错误。
System.ArrayTypeMismatchException 处理当数组类型不匹配时生成的错误。
System.NullReferenceException 处理当引用一个空对象时生成的错误。
System.DivideByZeroException 处理当除以零时生成的错误。
System.InvalidCastException 处理在类型转换期间生成的错误。
System.OutOfMemoryException 处理空闲内存不足生成的错误。
System.StackOverflowException 处理栈溢出生成的错误。
ArgumentNullException 参数不应为null时

自定义异常

自定义异常类通常继承自 Exception 类或其派生类。

自定义异常类的一般步骤:

  1. 创建自定义异常类: 创建一个继承自 Exception 类的新类,添加构造函数以及其他需要的属性和方法。
  2. 使用自定义异常: 在你的代码中,当发生特定的错误条件时,抛出自定义异常。

下面是一个简单的示例,演示如何创建和使用自定义异常:

using System;

//步骤1:创建自定义异常类
public class CustomException:Exception{
    //添加自定义的属性,可根据需要扩展
    public int ErrorCode{get;}
    
    //构造函数,可根据需要接受不同的参数
    public CustomException(int errorCode,string message):base(message){
        ErrorCode = errorCode;
    }
}

//【client】
class Program{
    static void Main(){
        try{
            //步骤2:使用自定义异常
            throw new CustomException(404,"Resource not found");
        }catch(Exception ex){
            Console.WriteLine($"An unexpected error occurred:{ex.Message}");
        }
    }
}

异常处理

异常是在程序执行期间出现的问题。异常提供了一种把程序控制权从某个部分转移到另一个部分的方式。

C# 异常处理时建立在四个关键词之上的:try、catch、finally 和 throw。

  • try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。

  • catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。

    无参数 catch 块 catch 或一般的 catch 块 catch(Exception ex) 必须是最后一个块。 如果在 catch 或 catch(Exception ex) 块之后还有其他 catch 块,编译器将给出错误。

  • finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。例如,如果您打开一个文件,不管是否出现异常文件都要被关闭。finally 块是一个可选块,应该在 try 或 catch 块之后。 无论是否发生异常,finally 块都将始终执行。 finally 块通常用于清理代码。注意 不允许添加多个 finally 块。 此外,finally 块也不能有 return、continue 或 break 关键字。这个跟 finally 块设计不符合。

  • throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。

    可以使用 throw 关键字抛出从 Exception 类派生的任何类型的异常。

    throw 使用 new 关键字创建任何有效异常类型的对象。 throw 必须跟从Excpetion派生出来的类型一起使用。

    eg: throw new NullReferenceException("Student object is null.");

try
{
   // 引起异常的语句
}
catch( ExceptionName e1 )
{
   // 错误处理代码
}
catch( ExceptionName e2 )
{
   // 错误处理代码
}
catch(Exception ex) //捕获所有
{
   // 错误处理代码
}
finally
{
   // 要执行的语句
}

LINQ

  1. 什么是LINQ?

答:LINQ(Language-Integrated Query)(集成语言查询)是一种用于.NET平台的查询和数据操作技术。

它是由微软开发的一组语言扩展和API,用于在编程语言中执行数据查询、转换和操作。LINQ 提供了一种统一的查询语法,称为查询表达式,让开发人员可以使用简洁的语法来编写查询表达式,而无需将查询逻辑嵌入到编程语言的代码中。这种语法类似于SQL语句,但在编译时进行类型检查,并集成到编程语言中。

使用查询表达式,开发人员可以以声明式的方式指定查询逻辑,而不必显式编写循环和条件语句。

LINQ 的概念基于一组通用的查询操作,如过滤、排序、分组、投影和聚合等。

它可以用于访问各种数据源,包括对象集合(例如数组和列表)、数据集(例如数据库表)和XML文档等。

LINQ 还提供了一组强大的标准查询操作符,如Where、OrderBy、GroupBy、Select、Join、Aggregate等,开发人员可以使用这些操作符来构建复杂的查询。此外,LINQ 还支持扩展方法语法,允许开发人员通过链式调用方法来编写查询。

总之,LINQ 是一种强大的查询和数据操作技术,简化了开发人员对各种数据源进行查询和操作的过程,提高代码的可读性可维护性。

  1. 关系图:

【一文详解】知识分享:(C#开发学习快速入门)_第22张图片

LINQ两种语法形式

LINQ(Language Integrated Query)提供了两种主要的语法风格:查询语法(Query Syntax)和方法语法(Method Syntax)。这两种语法风格都用于执行查询和操作集合的操作,只是表达方式上有所不同。

查询语法

查询语法是一种更接近自然语言的语法,类似于 SQL 查询语句。它使用关键字(如 fromwhereselect)来描述查询操作。

使用LINQ查询语法可以方便地对各种数据源(如数组、集合、数据库表等)进行查询、筛选、排序、分组等操作,同时还能够进行数据转换和投影。如下示例:

demo1.cs:

//下面是一个简单的LINQ查询表达式的示例:
//假设有一个包含若干个整数的数组,我们想要筛选出其中所有的偶数:

int[] numbers = { 1, 2, 3, 4, 5, 6 };
IEnumerable evenNumbers = from num in numbers
                               where num % 2 == 0
                               select num;
//在这个查询表达式中,我们使用了关键字“from”、“where”和“select”来描述查询的过程。
//其中,“from”关键字用于指定数据源,这里是整数数组“numbers”;
//“where”关键字用于指定筛选条件,这里是“num % 2 == 0”,即判断数字是否为偶数;
//“select”关键字用于指定查询结果的输出形式,这里是直接输出筛选出来的数字本身。

demo2.cs

//另外,LINQ查询表达式还可以进行更加复杂的数据处理操作,例如:
var students = new List
{
    new Student { Name = "Alice", Age = 20, Gender = Gender.Female },
    new Student { Name = "Bob", Age = 22, Gender = Gender.Male },
    new Student { Name = "Charlie", Age = 18, Gender = Gender.Male },
    new Student { Name = "David", Age = 19, Gender = Gender.Male },
    new Student { Name = "Eve", Age = 21, Gender = Gender.Female },
};

var query = from student in students
            where student.Age >= 20
            group student by student.Gender into genderGroups
            select new
            {
                Gender = genderGroups.Key,
                AverageAge = genderGroups.Average(s => s.Age),
                Names = string.Join(", ", genderGroups.Select(s => s.Name)),
            };

foreach (var result in query)
{
    Console.WriteLine($"{result.Gender}: {result.Names} (avg. age = {result.AverageAge})");
}

//在这个示例中,我们使用了LINQ查询表达式对一个名为“students”的学生列表进行了处理,首先筛选出了年龄大于等于20岁的学生,然后按照性别进行了分组,最终输出了每个性别的学生名单和平均年龄。
//这里使用了“group by”关键字来进行分组操作,并对每个分组进行了聚合计算和投影输出。
方法语法

方法语法使用方法调用链来实现查询。它使用一系列的 LINQ 方法,如 Where()OrderBy()Select() 等。

示例:

var result = peoples
    .Where(person => person.Age > 25)
    .OrderBy(person => person.Name)
    .Select(person => person);
区别
  1. 表达方式:
    • 查询语法更类似于自然语言,更容易阅读和理解。
    • 方法语法使用方法调用链,更像是编写传统的 C# 代码。
  2. 可读性:
    • 查询语法通常在简单的查询场景下更易读,特别是对于初学者。
    • 方法语法在复杂的查询链中可能更具可读性,特别是对于熟悉编程范式的开发者。
  3. 适用场景:
    • 查询语法适用于相对简单的查询,尤其是需要强调查询逻辑的情况。
    • 方法语法适用于更复杂的查询链,可以更灵活地进行组合和扩展。

运行时没有区别,编译后都是一样的 “查询语法”看起来更酷,但是“方法语法”更实用,因此.net开发者大部分还是用“方法语法”

LINQ常用的扩展方法(以方法语法示例)

Linq中提供了大量的类似where的扩展方法,简化数据处理,且这些扩展方法大部分都在System.Linq命令空间中。

Linq中所有的扩展方法几乎都是针对IEnummerable接口的,能返回集合的都返回IEnumerable,所以是可以把几乎所有方法“链式使用”

  1. where()方法

    数据源中的每一项数据都会经过predicate测试,如果针对数据源中的每一个元素,predicate执行的返回值为true,那么这个元素就会放到返回值中。

    where方法的实际参数是一个lambda表达式格式的匿名方法,方法的参数e表示当前判断的元素对象。参数的名字不一定非要叫e,不过一般lambda表达式中的变量名长度都不长。

    // example
    int[] nums = new int[]{3,99,88,77,7,8,9,66,15,7};
    IEnumerable items = nums.where(e => e>10);
    
  2. Count()方法: 获取数据条数

    //example
    int count1 = list.Count(e => e.salary > 1000 || e.Age < 30);
    int count2 = list.where(e => e.salary > 1000 || e.Age < 30).Count();
    
  3. Any()方法:是否至少有一条数据

    //example
    bool b1 = list.Any(e => e.salary > 8000);
    bool b2 = list.Where(e => e.salary > 8000).Any();
    
  4. 获取一条数据的相关API方法

    First()FirstOrDefault():这两个方法都用于获取序列中的第一个元素

    //First():返回序列的第一个元素,如果序列为空,会抛出异常
    var firstElement = list.First();
    
    //FirstOrDefault():返回序列的第一个元素,如果序列为空,则返回默认值(例如,对于 int 类型返回 0)
    var firstOrDefaultElement = list.FirstOrDefault();
    

    Single()SingleOrDefault():这两个方法用于获取序列中的唯一一个元素。

    //Single():返回序列中的唯一一个元素。如果序列为空或包含多个元素,会抛出异常。
    var singleElement = list.Single();
    
    //SingleOrDefault():返回序列中的唯一一个元素,如果序列为空,则返回默认值;如果序列包含多个元素,会抛出异常。
    var singleOrDefaultElement = list.SingleOrDefault();
    
  5. Take()方法: 获取多条数据

    Take 方法用于获取序列中指定数量的元素

    //example: Take(5) 表示从序列中取前面的 5 个元素。
    var takeElements = list.Take(5);
    
  6. 排序的相关API方法

    OrderBy()OrderByDescending():两个方法用于对序列进行升序(OrderBy)或降序(OrderByDescending)排列

    //OrderBy(): 按照指定的键升序排列
    var orderedList = list.OrderBy(item => item.Property);
    
    //OrderByDescending(): 按照指定的键降序排列
    var orderedListDesc = list.OrderByDescending(item => item.Property);
    

    ThenBy()ThenByDescending():这两个方法用于在已经进行排序的基础上,如果第一个键相同,再按照第二个键再次进行升序(ThenBy)或降序(ThenByDescending)排列。

    //ThenBy():在已经进行排序的基础上,按照指定的键升序排列。
    var thenByList = list.OrderBy(item => item.Property1).ThenBy(item => item.Property2);
    
    //ThenByDescending(): 在已经进行排序的基础上,按照指定的键降序排列。
    var thenByDescendingList = list.OrderByDescending(item => item.Property1).ThenByDescending(item => item.Property2);
    

    Reverse()方法:用于颠倒序列的顺序,即将第一个元素变成最后一个,第二个变成倒数第二个,以此类推。

    var reversedList = list.Reverse();
    
  7. Skip()方法: 限制结果集,用于跳过序列中的指定数量的元素,返回剩余的元素。

    //表示跳过序列中的前 5 个元素,返回剩余的元素。
    var skippedList = list.Skip(5);
    
    //Skip 方法通常与 Take 方法结合使用,以实现分页的效果。
    var page = 2; // 当前页码
    var pageSize = 10; // 每页元素数量
    var resultList = yourList.Skip((page - 1) * pageSize).Take(pageSize);
    
  8. 聚合函数相关API方法

    下面的yourList 可以是任何实现 IEnumerable 接口的序列,例如数组、列表等。

    Sum:

    Sum 方法用于计算序列中数值型元素的总和。

    var sum = yourList.Sum();
    

    Average:

    Average 方法用于计算序列中数值型元素的平均值。

    var average = yourList.Average();
    

    Min:

    Min 方法用于找到序列中数值型元素的最小值。

    var min = yourList.Min();
    

    Max:

    Max 方法用于找到序列中数值型元素的最大值。

    var max = yourList.Max();
    

    Aggregate:

    Aggregate 方法用于通过指定的累加函数对序列中的元素进行累积。

    TResult Aggregate(Func func)
    

    这个版本的 Aggregate 接受一个二元函数(Func),该函数定义了如何累积序列中的元素。函数接受两个参数,表示当前的累加值和下一个元素,返回值表示下一步的累加值。在没有初始累加值的情况下,Aggregate 将使用序列中的第一个元素作为初始累加值,然后逐个遍历剩余的元素进行累积。

    示例:

    var numbers = new List { 1, 2, 3, 4, 5 };
    
    var product = numbers.Aggregate((acc, next) => acc * next);
    
    Console.WriteLine(product); // 输出: 120
    
    //在这个例子中,Aggregate 以第一个元素 1 作为初始累加值,然后通过乘法逐个累积后续的元素,得到最终的结果 120。
    

    Aggregate(带初始值):

    Aggregate 方法还可以带一个初始值,作为累加的起始值。

    TAccumulate Aggregate(TAccumulate seed, Func func)
    

    这个版本的 Aggregate 接受一个初始累加值 seed 和一个二元函数(Func)。与前一个版本不同,这个版本明确指定了初始累加值,然后逐个遍历序列中的元素进行累积。

    示例:

    var numbers = new List { 1, 2, 3, 4, 5 };
    
    var productWithSeed = numbers.Aggregate(1, (acc, next) => acc * next);
    
    Console.WriteLine(productWithSeed); // 输出: 120
    
    //在这个例子中,Aggregate 以初始累加值 1 开始,然后通过乘法逐个累积后续的元素,得到最终的结果 120。
    

    Join:

    Join 方法用于将序列中的元素连接为一个字符串。

    var joinedString = yourList.Join(", ");
    
  9. GroupBy()方法: 用于按照指定的键进行分组

    //example
    var students = new List
    {
        new Student { Name = "Alice", Grade = "A" },
        new Student { Name = "Bob", Grade = "B" },
        new Student { Name = "Charlie", Grade = "A" },
        new Student { Name = "David", Grade = "B" },
        new Student { Name = "Emily", Grade = "A" }
    };
    
    var groupedStudents = students.GroupBy(student => student.Grade);
    
    foreach (var group in groupedStudents)
    {
        Console.WriteLine($"Grade: {group.Key}");
        foreach (var student in group)
        {
            Console.WriteLine($"  {student.Name}");
        }
    }
    
    //输出结果如下:
    Grade: A
      Alice
      Charlie
      Emily
    Grade: B
      Bob
      David
    
  10. 投影及相关API

    在 LINQ(Language Integrated Query)中,投影(Projection)是指从源序列中选择或变换元素的过程。通过投影,你可以从集合中选择特定的字段、计算新的值,或者创建新的对象。在 C# 中,投影通常使用 Select() 方法进行,如下示例:

    //基础示例:
    var fruits = new List { "apple", "orange", "banana", "grape" };
    
    var uppercasedFruits = fruits.Select(fruit => fruit.ToUpper());
    
    foreach (var result in uppercasedFruits)
    {
        Console.WriteLine(result);
    }
    
    //输出结果为:
    APPLE
    ORANGE
    BANANA
    GRAPE
    

    LINQ 允许你创建匿名类型来组合源序列中的多个字段或属性,如下示例:

    var students = new List
    {
        new Student { Name = "Alice", Grade = "A" },
        new Student { Name = "Bob", Grade = "B" },
        new Student { Name = "Charlie", Grade = "A" }
    };
    
    var studentDetails = students.Select(student => new {student.Name,student.Grade});
    
    foreach (var detail in studentDetails)
    {
        Console.WriteLine($"Name: {detail.Name}, Grade: {detail.Grade}");
    }
    
    //输出结果:
    Name: Alice, Grade: A
    Name: Bob, Grade: B
    Name: Charlie, Grade: A
    

    可以通过 Select() 创建新的自定义类型,将源序列中的元素映射到这个新类型,如下示例:

    var employees = new List
    {
        new Employee { Id = 1, Name = "John", Department = "HR" },
        new Employee { Id = 2, Name = "Alice", Department = "IT" },
        new Employee { Id = 3, Name = "Bob", Department = "Finance" }
    };
    
    var employeeDetails = employees.Select(employee => new EmployeeDetails
    {
        Id = employee.Id,
        FullName = $"{employee.Name} - {employee.Department}"
    });
    
    foreach (var detail in employeeDetails)
    {
        Console.WriteLine($"Employee ID: {detail.Id}, Full Name: {detail.FullName}");
    }
    
    //输出结果:
    Employee ID: 1, Full Name: John - HR
    Employee ID: 2, Full Name: Alice - IT
    Employee ID: 3, Full Name: Bob - Finance
    
    1. 集合转换相关API

      ToArray()方法:用于将 IEnumerable 转换为数组类型 T[],示例如下:

      var numbers = Enumerable.Range(1, 5);//Enumerable.Range(1, 5) 生成的是一个整数序列,它属于 IEnumerable 类型。在 C# 中,IEnumerable 是表示可枚举集合的接口,它表示一个可以按顺序逐个访问其元素的集合。
      //虽然这个序列的底层实现是由 Enumerable.Range 方法创建的,但它并不是一个数组(Array)类型。它是一个通过 IEnumerable 接口提供迭代功能的对象。
      int[] arrayNumbers = numbers.ToArray();
      

      ToList()方法:用于将 IEnumerable 转换为 List 类型,示例如下:

      var fruits = new[] { "apple", "banana", "orange" };
      List fruitList = fruits.ToList();
      
    2. 链式调用

      链式调用是指在 LINQ 查询中使用一系列的操作方法,每个操作方法都返回一个新的 IEnumerable,从而可以在其上继续进行操作。常见的链式调用包括 Where()Select()OrderBy()GroupBy()Skip() 等,如下示例:

      var employees = new List
      {
          new Employee { Id = 1, Name = "Alice", Age = 25, Salary = 50000 },
          new Employee { Id = 2, Name = "Bob", Age = 30, Salary = 60000 },
          new Employee { Id = 3, Name = "Charlie", Age = 25, Salary = 55000 },
          new Employee { Id = 4, Name = "David", Age = 35, Salary = 70000 }
      };
      
      var result = employees
          .Where(employee => employee.Id > 2)
          .GroupBy(employee => employee.Age)
          .OrderBy(group => group.Age)
          .Select(group => new
          {
              Age = group.Age,
              Count = group.Count(),
              AverageSalary = group.Average(employee => employee.Salary)
          })
          .Take(3);
      
      //在这个示例中,链式调用首先使用 Where 筛选出 Id > 2 的员工,然后使用 GroupBy 按照年龄分组,接着使用 OrderBy 对分组按照年龄排序,最后使用 Select 投影出年龄、人数、平均工资的匿名类型,并使用 Take 取前 3 条结果。
      

LINQ to Object步骤

  1. 引用 LINQ 命名空间:在代码文件的顶部,使用 using 关键字引用 System.Linq 命名空间,以便使用 LINQ 相关的类型和扩展方法。

  2. 创建数据源:定义一个数据源,可以是对象集合、数组、数据集或其他实现了相应接口的数据结构。

  3. 构建 LINQ 查询表达式:使用 LINQ 的查询语法或方法语法来构建查询表达式,指定要过滤、排序、投影等的操作。

  4. 执行查询:通过调用适当的查询操作符来执行查询。

    查询操作可以是即时执行的(立即返回结果),也可以是延迟执行的(在需要时才计算结果)。

  5. 处理查询结果:使用循环、条件语句或其他操作来处理查询结果,并获取所需的数据。

下面是简单的 LINQ To Object示例,演示如何对一个整数列表进行过滤和排序:

using System;
using System.Linq;//引入LINQ

public class Program
{
    public static void Main()
    {
        // 创建数据源
        int[] numbers = { 5, 1, 4, 2, 3 };

        // 构建 LINQ 查询表达式
        var query = from num in numbers
                    where num % 2 == 0 // 过滤偶数
                    orderby num descending // 按降序排序
                    select num; // 投影选择的数字

        // 执行查询并处理结果
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}

//以上示例代码使用 LINQ 查询语法,从一个整数数组中过滤出偶数,并按降序排序。然后通过循环打印出筛选后的结果。
//通过以上步骤,你可以开始使用 LINQ 来进行各种查询和数据操作。

Visual Studio 2019连接MySQL数据库

  1. 要想在 Visual Studio 2019中使用MySQL数据库,首先需要下载MySQL的驱动:

    • mysql-connector-odbc-8.0.20-winx64.msi
      链接: https://dev.mysql.com/downloads/connector/odbc/.

    • mysql-for-visualstudio-1.2.9.msi
      链接:https://dev.mysql.com/downloads/windows/visualstudio/

    • mysql-connector-net-8.0.20.msi
      链接:https://dev.mysql.com/downloads/connector/net/8.0.html
      自行下载即可
      下载完后按照以上顺序安装

  2. 安装完后重启visual studio

    然后点击菜单栏的视图->服务器资源管理器

    【一文详解】知识分享:(C#开发学习快速入门)_第23张图片

    然后数据连接->添加连接

    【一文详解】知识分享:(C#开发学习快速入门)_第24张图片

    你就会发现有MySQL 的选项了,进入里面配置数据库相关信息即可。若还是没有MySQL选项,尝试选择更高版本的framework

    【一文详解】知识分享:(C#开发学习快速入门)_第25张图片

Visual Studio 2019 中找不到Linq to SQL 类

在 Visual Studio 2019 中,Linq to SQL 已被标记为“过时”的技术,但仍然可以使用。如果您在 Visual Studio 2019 中找不到 Linq to SQL 类,请按照以下步骤操作:

  1. 确保已安装 .NET 框架版本 3.5 或更高版本。
  2. 在解决方案资源管理器中,右键单击项目,选择“添加”>“新建项”。
  3. 在“添加新项”对话框中,选择“数据”类别,然后选择“LINQ to SQL 类”模板。
  4. 给 Linq to SQL 类命名并点击“添加”按钮。
  5. 在 Server Explorer 中连接到数据库。
  6. 在“服务器资源”窗格中,展开数据库,然后将表拖动到 Linq to SQL 类设计器中,以创建对应的实体类和 DataContext 类。

如果您仍然无法找到 Linq to SQL 类,请确保已安装 Linq to SQL 组件。在 Visual Studio 2019 中,您可以通过转到“工具”>“获取工具和功能”>“修改”>“单个组件”>“SDK、库和框架”>“.NET 框架 4.x 开发人员工具”>“Linq to SQL 工具”来安装 Linq to SQL 组件。【直接搜索相应的工具名即可】

LINQ To Sql 步骤

LINQ to SQL 是一种在.NET Framework中使用LINQ查询关系数据库的技术。它通过将数据库架构映射到对象模型来简化数据库的访问操作。

即: ORM(Object Relation Mapping.

下面是使用 LINQ to SQL 的一般步骤和示例:

  1. 创建数据库表结构:在关系数据库中创建并定义操作的表和字段。
  2. 引用 LINQ 命名空间:在代码文件的顶部,使用 using 关键字引用 System.Linq 命名空间,以便使用 LINQ 相关的类型和扩展方法。
  3. 连接到数据库:添加一个数据连接,连接到目标数据库。
  4. 创建对象模型:如果是sql server数据库,可以使用 Visual Studio 工具中的 “LINQ to SQL Classes”,从连接的数据库中提取架构信息,自动生成与数据库表对应的类和属性。不然就手动编写创建相应的数据对象模型。
  5. 构建查询:利用生成的对象模型,使用 LINQ 查询语法或方法语法来构建查询。
  6. 执行查询和操作:通过对查询结果执行相关操作,例如排序、更新、插入或删除数据。

下面是一个简单的 LINQ to SQL 示例,演示如何查询数据库中的数据:

using System;
using System.Linq;//引入

public class Program
{
    public static void Main()
    {
        // 创建 LINQ to SQL 数据上下文
        // CRUD都找这个上下文对象
        using (var db = new MyDataContext())
        {
            // 构建查询(查询语法格式)
            var query = from c in db.Customers
                        Where c.City == "London"
                        select c;
            
            //构建查询(方法语法格式)
            var query = db.Customers.Where(c => c.City == "London");

            // 执行查询并处理结果
            foreach (var customer in query)
            {
                Console.WriteLine(customer.CustomerName);
            }
        }
    }
}

//在此示例中,我们首先创建了一个 `MyDataContext` 的 LINQ to SQL 数据上下文对象,该对象表示与数据库的连接。然后,我们构建了一个查询,查询 `Customers` 表中城市为 “London” 的客户名。最后,我们通过循环打印出查询结果中的客户名。

//通过以上步骤,你可以开始使用 LINQ to SQL 来访问和操作关系数据库,以更简洁和直观的方式进行数据查询。

Entity Framework(EF)

如果要操作Mysql数据库,使用第三方库如Entity Framework,也是可以实现.Net环境下对MySQL数据库的访问和操作。

以下是使用Entity Framework配合MySQL进行操作的基本步骤:

  1. 打开Visual Studio,创建或打开已有项目。
  2. 打开工具 -> NuGet包管理器 -> 管理解决方案的NuGet程序包
  3. 在打开的NuGet包管理器中搜索MySql.Data.EntityFramework并安装。
  4. 在你的模型类上添加[System.Data.Linq.Table(Name = “your_table_name”)]特性,你的列的属性添加[System.Data.Linq.Column(IsPrimaryKey = true, IsDbGenerated = true)]特性。
  5. 创建你的数据上下文类继承自DbContext,并且创建对应的DbSet属性。

以上就是一种方式来连接MySQL和EF,另外你还可以通过Code FirstDatabase First或者Model First来创建模型。

然而,如果还是想用类似LINQ to SQL的方式来操作MySQL数据库,可以选择Dapper等其它比较流行的ORM框架。


C# 注意事项:

  1. 如下的各个成对写法是等效的:

    【一文详解】知识分享:(C#开发学习快速入门)_第26张图片

    为什么要有两种写法呢?

    因为其中的小写类型开头的是c#定义的,而大写类型开头的则是.Net framework的CLR里的CTS定义的。

    这样的话不同类型语言到了.Net框架后才可以达到一个相同的定义。

    所以小写属于c#写法,大写属于.Net框架写法。

  2. c#中的Struct数据类型类似Class类型,但区别是Class是属于引用类型,Struct是属于值类型

  3. 值类型引用类型在内存(栈和堆)中的分配形式:

    c#的值类型是在栈中分配空间,即值存储在栈中。

    c#的引用类型是在堆中分配空间,栈中只保存其引用。即栈中保存指向堆的地址,堆中保存真实的对象和数据整体。

    如下:

    【一文详解】知识分享:(C#开发学习快速入门)_第27张图片

  4. 实例化一个对象时,从内存角度分析:

    case1:

    【一文详解】知识分享:(C#开发学习快速入门)_第28张图片

    case2:

    【一文详解】知识分享:(C#开发学习快速入门)_第29张图片

  5. 方法调用时形参的值传递(默认行为,传递变量的副本)介绍:

    先说结论: 值类型的形参引用类型的形参在方法调用都是值传递,跟形参的类型是值类型或者是引用类型都无关。

    所谓值传递: 即copy一份副本出来调用。

    对于值类型的形参:

    【一文详解】知识分享:(C#开发学习快速入门)_第30张图片

    对于引用类型的形参(方法体中不带有new一个实例对象):方法改变的是同一个对象

    【一文详解】知识分享:(C#开发学习快速入门)_第31张图片

    对于引用类型的形参(方法体中带有new一个实例对象):方法改变的是方法中创建的新对象。

    【一文详解】知识分享:(C#开发学习快速入门)_第32张图片

  6. 方法调用时形参的引用传递:

    由于默认值传递,要实现引用传递只能做特殊处理。

    • 法一:在形参变量的前面加上ref关键字,同时传入实参时也在实参变量前面加上ref关键字。
    • 法二:在形参变量的前面加上out关键字,同时传入实参时也在实参变量前面加上out关键字。

    这两种特殊处理的区别:

    • ref侧重于改变某一个值; out侧重于输出一个值

    • 法二需要在方法体里面添加一句形参变量的初始化语句才不会报错,如下:

      【一文详解】知识分享:(C#开发学习快速入门)_第33张图片

  7. 对于接收数字的形参变量的类型定义:

    只要不参与运算,方法的形参接收的数字均定义为string类型。

    只有参与运算,形参的数字才定义为数字类型。

  8. 小数会默认被认为是double类型,若想标识为float,后面就要加f;同理,若要标识为decimal,后面就要加m,如下所示:

    【一文详解】知识分享:(C#开发学习快速入门)_第34张图片

  9. 在类的成员方法中声明的本地变量(局部变量),在声明时还要赋值,因为不赋值其是没有默认值的。如: int age = 5.

    但是.若是定义在类中的成员变量(静态变量或实例变量),则不需要赋值,其是有默认值的.【Java中也一样

  10. c#中没有全局变量的说法,即没法在类之外定义变量。

    所以全局变量的思路往往采用类的静态成员的思路解决。

  11. c#的构造函数注意事项:

    • 名字和类名相同。
    • 没有返回值,void也不行。
    • 使用public作为权限修饰符,因为private的话就不能用new来创建这个类的对象了。
    • 没有手动写有参构造方法时,一般编译器会默认生成一个无参构造函数(并赋值默认值),而如果手动写有参构造方法,则编译器不会默认生成一个无参构造函数,需要另外手动写无参构造函数。
  12. c#的完全限定名 = 命名空间 + 类名。

    当打印一个实例对象时,打印的结果就是一个完全限定名。

  13. Object类的ToString()方法:用于将当前对象实例以字符串的形式来表示。

  14. Visual Studio怎么快速多行注释和取消注释?

    用鼠标选中要注释的代码,然后,先按 Ctrl - K 组合键,再按 Ctrl - C 组合键进行注释。

    如果要取消注释,就选中被注释的代码,然后,先按 Ctrl - K 组合键,再按 Ctrl - U 组合键即可取消注释。

  15. C#中的internal关键字限制的访问范围是?

    即同一个项目内部是可以访问的。

    或者同一个dll文件内部是可以访问的。

    因为通常来讲一个项目生成一个dll文件

  16. C#中的extern关键字?

    在 C# 中,extern 关键字用于声明一个外部方法。它用于指示编译器该方法的实现是在外部的,即在当前的代码文件之外,通常是在其他的本机语言(如 C++)或外部库中实现的。使用 extern 关键字声明的方法不需要提供方法体,因为它的实现在其他地方。这样可以使 C# 代码与其他语言或库进行交互。

  17. 在C#中,如果你在数据类型后面加上问号(?),这表示该数据类型是可空的。这特别适用于值类型(Value Types),例如整数(int)、双精度浮点数(double)等。通过将其声明为可空,你可以将其值设置为null,表示缺少数值。

    int? nullableInt = null;
    //在这里,nullableInt 是一个可空整数,可以包含一个整数值,也可以是null。这在处理数据库查询等场景中非常有用,因为数据库中的某些字段可能允许为空。
    
  18. 在C#中,?.运算符??运算符是用于处理可能为null的引用类型的特殊运算符:

    ?.运算符(Null 条件成员访问运算符):

    ?.运算符允许您在访问引用类型成员之前进行空值检查。

    它的作用是如果左侧的操作数为null,则整个表达式的结果为null,否则(不为Null)才会继续访问成员。

    示例:

    int? length = name?.Length;
    Console.WriteLine(length);  // 输出:null
    //在上面的示例中,如果`name`为null,则`name?.Length`表达式的结果将为null,而不会抛出NullReferenceException。这在避免空引用异常的情况下很有用。
    

    ??运算符(null 合并运算符): ??运算符用于在表达式中处理可能为null的引用类型,并提供一个默认值,当左侧的操作数为null时使用该默认值。
    示例:

    string displayName = name ?? "Guest";
    Console.WriteLine(displayName);  // 输出:Guest
    
    //在上面的示例中,如果`name`为null,则`name ?? "Guest"`表达式的结果将为"Guest",因为左侧的操作数为null,所以使用了默认值"Guest"。
    

    这些运算符也可以结合使用,如下示例:

    string username = user?.Name ?? "Guest";
    Console.WriteLine(username);
    
    //在上面的示例中,如果`user`为null或者`user.Name`为null,那么`username`将被赋值为"Guest"。否则,它将被赋值为`user.Name`的值。
        
    //这样可以防止使用null值引发异常,并提供一个默认值。
    
  19. 所谓二进制数据的本质就是字节数组;

    字节数组是存储和传输二进制数据的一种常见方式。

    通过使用字节数组,我们可以有效地处理和管理二进制数据,并在计算机系统中进行传输和存储。

The End!!创作不易,欢迎点赞/评论!!欢迎关注个人GZH!!

你可能感兴趣的:(c#,c#,经验分享,asp.net,.net,visual,studio,知识分享)