C#基础知识与新特性

C#简介

C#(读作“See Sharp”)是一种新式编程语言,不仅面向对象,还类型安全。

C# 程序在 .NET 上运行,而 .NET 是名为公共语言运行时 (CLR) 的虚执行系统和一组类库。 CLR 是 Microsoft 对公共语言基础结构 (CLI) 国际标准的实现。 CLI 是创建执行和开发环境的基础,语言和库可以在其中无缝地协同工作。

用 C# 编写的源代码被编译成符合 CLI 规范的中间语言 (IL)。 IL 代码和资源(如位图和字符串)存储在扩展名通常为 .dll 的程序集中。 程序集包含一个介绍程序集的类型、版本和区域性的清单。

执行 C# 程序时,程序集将加载到 CLR。 CLR 会直接执行实时 (JIT) 编译,将 IL 代码转换成本机指令。 CLR 可提供其他与自动垃圾回收、异常处理和资源管理相关的服务。 由 CLR 执行的代码有时称为“托管代码”。 “非托管代码”编译为面向特定平台的本机语言。

语言互操作性是 .NET 的一项重要功能。 C# 编译器生成的 IL 代码符合公共类型规范 (CTS)。 通过 C# 生成的 IL 代码可以与通过 .NET 版本的 F#、Visual Basic、C++ 生成的代码进行交互。

语法糖与新特性

dynamic
让C#具有了弱语言类型的特性。编译器在编译的时候不再对类型进行检查,编译期默认dynamic对象支持你想要的任何特性。
var和dynamic完全是两个概念,根本不应该放在一起做比较。
var实际上是编译期抛给我们的“语法糖”,一旦被编译,编译期会自动匹配var 变量的实际类型,并用实际类型来替换该变量的申明,这看上去就好像我们在编码的时候是用实际类型进行申明的。
而dynamic被编译后,实际是一个object类型,只不过编译器会对dynamic类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期。
1、如果编译时函数名称确定,对象类型运行时确定,那么运用dynamic是一个好主意。
2、如果编译时函数名称确定,对象类型在编译时也确定,那就既不需要反射也不需要dynamic。
3、如果函数名称在运行时才能确定的话,那函数名称就是一个字符串,必须使用反射来完成。

record
实现了基于属性值的相等性比较,不再使用默认的引用,并且重写了 ==/!= operator 和 GetHashCode。
为了方便调试,重写了 ToString 方法,也提供了 PrintMembers 方法来实现比较方便只显示某些比较重要的参数。
record 提供了 with 表达式来方便的克隆一个新的对象。
以前用class new出来的2个实例,哪怕值一样,他们也是不相等。但record是相等的
与类不同的是,它是基于值相等而不是唯一的标识符——对象引用。
如果你需要整个对象都是不可变的,且行为像一个值,那么你也可考虑将其声明为一个record类型。

public record Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

//如果Person打算改变他的姓氏(last name),
//我们就需要通过拷贝原来数据,并赋予一个不同的last name值来呈现一个新Person。
//这种技术被称为非破坏性改变。
var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
//为了帮助进行这种类型的编程,针对records就提出了with表达式,
//用于拷贝原有对象,并对特定属性进行修改:
var otherPerson = person with { LastName = "Torgersen" };

//只是进行拷贝,不需要修改属性,那么无须指定任何属性修改
Person clone = person with { };
  • 记录只能从记录继承,不能从类继承,也不能被任何类继承。
  • record不能定义为static的,但是可以有static成员

Init
在以前的C#版本里面,如果需要定义一个不可修改的的类型的做法一般是:声明为readonly,并设置为只包含get访问器,不包含set访问器。
init(只初始化属性或索引器访问器):只在对象构造阶段进行初始化时可以用来赋值,算是set访问器的变体,set访问器的位置使用init来替换。
总的功能就是扩大readonly实例字段的赋值方式

使用了init关键字的属性,允许调用方在初始化的时候对其赋值一次,之后如果再次赋值,就会出现编译错误。

public string Name { get; init; }  

格式化字符串:(“你好啊!{0}”,name)

字符串插值:“你好啊!{name}”

格式字段:(:Axx) A 为说明符,指定格式化类型,xx 为精度说明符

string s = string.Format("{0:F2}", 12.56789);   //输出12.57

{
    语句1;
    语句2;
}

局部函数/内置方法

public void A()

{
   int B(int c)

   {
       return c+1;
   }
   B(1);
}

命令参数

// 指定给参数C传值
B(c:1)

out参数

void D(ref int number)
{
    number = 10;
}
int number = 5;
D(ref number);

// out无需在外面重新定义变量
void F(out int i)
{
    i = 10;
}
F(out int i);

ref是有进有出,out是只出不进。

equals方法比较的是两个对象的内容是否一致.==也就是比较引用类型是否是对同一个对象的引用。

==操作符判断的是栈中的值,Equlas判断的是堆中的值。

对于引用类型而言,其栈中存储的是对象的地址,那么==就是比较两个地址是否相等。

params参数

//可变参数,写在最后
int B(int c,params int[] ints)
{

      return ints[0];

}

B(1,2,3,4,5);
   
// 此时 C为1,ints为2,3,4,5
   

引用静态类

using static.xxx
//可以直接使用xxx的静态属性,不用在xxx.a

自动属性初始化语句

public double Length { get; set; } = 42.5;

空条件运算符

Student[] students = null;
//你可以使用空条件运算符来避免空引用异常,如以下示例所示。
int? studentCount = students? . Length;
Student firstStudent = students?[0];

元组

// 返回多个类型
(string, int)  CreateSampleTuple()
{
      return ("Paul", 39);
}

堆和栈

程序运行时,它的数据必须存储在内存中。一个数据项需要多大的内存、存储在什么地方,以及如何存储都依赖于该数据项的类型。运行中的程字使用两个内存区域来存储数据:栈和堆。

是一个内存数组,是一个LIFO ( Last-In First-Out,后进先出)的数据结构。

存储:值类型的值,程序当前的运行环境,方法的参数。

是一块内存区域,在堆里可以分配大块的内存用于存储某种类型的数据对象。与栈不同,堆里的内存能够以任意顺序存入和移除

虽然程序可以在堆里保存数据,但并不能显式地删除它们。CLR的自动垃圾收集器在判断出程序的代码将不会再访问某数据项时,会自动清除无主的堆对象。

值类型 和 引用类型

值类型只需要一段单独的内存,用于存储实际的数据。

引用类型需要两段内存。

第一段存储实际的数据,它总是位于堆中。

第二段是一个引用,指向数据在堆中的存放位置。

const、readonly

const常量在声明时必须初始化。在声明后不能改变

const是一个编译期常量,readonly是一个运行时常量。
const只能修饰基元类型、枚举类型或字符串类型,readonly没有限制。
关于第一个区别,因为const是编译期常量,所以它天然就是static的。
之所以说const变量的效率高,是因为经过编译器编译后,我们在代码中引用const变量的地方会用const变量所对应的实际值来代替

readonly的全部意义在于,它在运行时第一次被赋值后将不可以改变。当然,“不可以改变”分为两层意思:
1)对于值类型变量,值本身不可改变(readonly,只读)。
2)对于引用类型变量,引用本身(相当于指针)不可改变。
引用本身不可改变,引用所指的实例的值,却是可以改变的

索引器

//索引器允许类或结构的实例像数组一样进行索引。  
//类似于属性,在定义的时候采用get和set方法  
public class Test  
    {  
        //  定义语法  
        //  [访问修饰符] 数据类型 this [数据类型  变量]  
        //   get{}  
        //   set{}  
        
        public string OrderNo { get; set; }

        public string this[int index]
        {

            get//索引关键字  
            {
                switch (index)
                {
                    case 0:
                        return OrderNo;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException("index");
                }
            }
            set//索引关键字  
            {
                switch (index)
                {
                    case 0:
                        OrderNo = value;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException("index");
                }
            }
        }
    }  

构造函数是和类同名.没有返回值.
析构函数是在类名前加~.也没有返回值.

构造函数上在对象创建时执行.
析构函数是在程序结束时执行.

析构函数也就是终结器

一般时候析构函数里面写的都是一些资源回收之类的东西.

类继承

类继承其基类的成员。 继承意味着一个类隐式包含其基类的几乎所有成员。 类不继承实例、静态构造函数以及终结器。 派生类可以在其继承的成员中添加新成员,但无法删除继承成员的定义。 但可以使用new屏蔽继承成员

基类访问器base

尽管已经被屏蔽了,依旧可以使用base来访问。

虚方法与重写

方法,属性,索引器,实践都可以被重写

// 转换
派生类 a=new 派生类();
基类 b=(基类)a;
//如果基类里面的虚方法被派生类重写
//则 b.xxx 会执行a.xxx
//总而言之,会执行最高派生类的

继承的顺序:成员初始化–>基类构造函数–>派生类构造函数

构造函数初始化语句:默认情况下,在构造对象时,将调用基类的无参数构造函数。但构造函数可以重载,所以基类可能有一个以上的构造函数。
如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。

第一种形式使用关键字base并指明使用哪一个基类构造函数。
第二种形式使用关键字this并指明应该使用当前类的哪一个构造函数。

    public class A
    {
        public A()
        { 
        }
        public A(string a,int b)
        {
           Console.WriteLine("Aab");
        }
    }

    public class B : A
    {
        public B():base() //无参
        { 
        }
        public B(string a, int b) : base(a,b)
        {
           Console.WriteLine("Bab");
        }

        public B(int b) : this("ssss", b)
        {
           Console.WriteLine("Bb");
            //Aab,Bab,Bb
        }
    }


抽象成员

抽象成员是指设计为被覆写的函数成员。抽象成员有以下特征。

1、必须是一个函数成员。也就是说,字段和常量不能为抽象成员。

2、必须用abstract修饰符标记。

3、不能有实现代码块。抽象成员的代码用分号表示。

共有4种类型的成员可以声明为抽象的:
方法、属性、事件、索引器。

其他重要事项如下。
1、尽管抽象成员必须在派生类中用相应的成员覆写,但不能把virtual 修饰符附加到
abstract修饰符。

2、类似于虚成员,派生类中抽象成员的实现必须指定override修饰符

抽象类

抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。

不能创建抽象类的实例

抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带
实现的成员的任意组合。
任何派生自抽象类的类必须使用override关键字实现该类所有的抽象成员,除非派生类
自己也是抽象类

    public abstract class A
    {
        public abstract void DoWork(int i);   //抽象成员只能声明在抽象类中 
        public abstract int PropertyValue
        {
            get;
            set;
        }
        // 抽象类中可以包含普通带实现的成员
        public void Output()
        {
            Console.WriteLine("I am Abstract Class A");
        }
    }
    public class MyClass : A
    {
        int MyValue;
        //派生类必须全部实现基类中所有抽象成员
        public override void DoWork(int i)    
        {
            Console.WriteLine("I am Class MyClass. My Value is {0}", i);
        }
        public override int PropertyValue

        {
            get { return MyValue; }
            set { MyValue = value; }
        }
    }

密封类

密封类只能被用作独立的类,它不能被用作基类。

密封类使用sealed修饰符标注

静态类

常见用途:创建一个包含一组数学方法和值的数学库。

1、类本身必须标记为static。

2、类的所有成员必须是静态的。

3、类可以有一一个静态构造函数,但不能有实例构造函数,因为不能创建该类的实例。

4、静态类是隐式密封的,也就是说,不能继承静态类。

可以使用类名和成员名,像访问其他静态成员那样访问静态类的成员。从C# 6.0开始,也可
以通过使用using static指令来访问静态类的成员,而不必使用类名。

扩展方法

1、声明扩展方法的类必须声明为static。

2、扩展方法本身必须声明为static。

3、扩展方法必须包含关键字this作为它的第一个参数类型,并在后面跟着它扩展的类的
名称。

 public class A
    {
        int a1, b1;
        public A(int a, int b)
        {
            a1 = a;
            b1 = b;
        }
        public int sum()
        {
            return a1 + b1;
        }
    }

    public static class B
    {
        public static string tostringSum(this A a)
        {
            return ("和为:"+a.sum().ToString());
        }
    }
    
    // 调用
    A a = new A(1, 2);
    string str = a.tostringSum();

移位运算符

14<<3

14的二进制为:1110

向左移位3,先补0:0001110

最终:11100000

类型转换

可以为自己的类和结构定义隐式转换和显式转换

public 和static修饰符是所有用户定义的转换所必需的。

explicti 表示显式转换,如从 A -> B 必须进行强制类型转换(B = (B)A)

implicit 表示隐式转换,如从 B -> A 只需直接赋值(A = B)

// implicit  operator           隐式运算符
// explicit operator            显式运算符

   public class A
   {
        int a1, b1;
        public A(int a, int b)
        {
            a1 = a;
            b1 = b;
        }

        public static implicit operator B(A a)
        {
            return new B();
        }
        
        public static implicit operator A(string s)
        {
            return new A(1,int.Parse(s));
        }
    }

    public  class B
    {
        public static explicit operator A(B b)
        {
            return new A(1,2);
        }

    }

    public class C
    {
        public void zzz()
        {
            A a = (A)new B();
            a = "1";
            B b = new A(1,2);
        }
    }  

typeof

typeof运算符返回作为其参数的任何类型的System. Type对象。

通过这个对象,可以了解类型的特征。

对任何已知类型,只有一个System. Type对象

你不能重载typeof运算符

using System.Reflection; // 使用反射命名空间来全面利用检测类型信息的功能

Type t = typeof ( class );

GetType方法也会调用typeof运算符,该方法对每个类型的每个对象都有效

class.GetType();

Name name =new Name();
System.Type t = typeof(Name);
System.Type t1 = name.GetType();

nameof运算符

nameof运算符返回一个表示传入参数的字符串

string str = nameof(name);
// 返回Name

标签与goto

标签语句由一个标识符后面跟着一个冒号再跟着语句组成。

它的形式:Identifier: Statement
标签语句的执行如同标签不存在一样,并仅执行Statement部分。
1、允许控制从代码的其他部分转移到该语句。

2、标签语句只允许用在块内部。

  • 不可以是关键字
  • 不可以和在重叠范围内和另一个标签标识符相同
 public void A()
        {
            {
                int i = 0;
                Identifier: i++;
                
                // 当程序执行到此,会返回执行i++,这里是个死循环
                goto Identifier;
            } 
        }

goto语句无条件地将控制转移到一个标签语句。

  • goto语句可以跳到它本身所在块内的任何标签语句,或跳出到任何它被嵌套的块内的标签语句。
  • goto语句不能跳入任何嵌套在该语句本身所在块内部的任何块。

使用goto语句是非常坏的习惯,因为它会导致弱结构化的、难以调试和维护的代码。

结构

结构是程序员定义的数据类型,与类非常类似。

类是引用类型,而结构是值类型;

结构是隐式密封的,这意味着不能从它们派生其他结构。

结构类型的变量不能为null;
两个结构变量不能引用同一对象。

结构可以有实例构造函数和静态构造函数,但不允许有析构函数。

对结构进行分配的开销比创建类实例小,所以使用结构代替类有时可以提高性能,但要注意
装箱和拆箱的高昂代价。

枚举

创建枚举的理由之一,就是为了代替使用实际的数值,将0值作为枚举的默认值

    public enum InfoType
    {
        [Description("信息")]
        Message = 0,
        [Description("异常")]
        Exception = 1
    }

数组与复制

数组一旦创建,大小就固定了,C#不支持动态数组

Clone方法为数组进行浅复制,也就是说,它只创建了数组本身的克隆。

如果是引用类型数组,它不会复制元素引用的对象。
克隆值类型数组会产生两个独立数组。
克隆引用类型数组会产生指向相同对象的两个数组。

浅拷贝:将对象中的所有字段复制到新的对象(副本)中。其中,值类型字段的值被复制到副本中后,在副本中的修改不会影响到源对象对应的值。而引用类型的字段被复制到副本中的是引用类型的引用,而不是引用的对象,在副本中对引用类型的字段值做修改会影响到源对象本身
深拷贝:同样,将对象中的所有字段复制到新的对象中。不过,无论是对象的值类型字段,还是引用类型字段,都会被重新创建并赋值,对于副本的修改,不会影响到源对象本身。

委托

1、声明:delegate 返回类型 委托名称(参数签名);

public delegate void delUpdate(int i)

没有方法体

2、创建委托对象与赋值

委托名称 变量名;

变量名 = new 委托名称(实例方法);

delUpdate d = new delUpdate(test1);

3、增加方法

可以使用 变量名+=实例方法;

4、调试方法

可以像调用方法一样使用

也可以使用委托的Invoke方法

//委托定义,相当于一个函数签名,函数指针
public delegate void delUpdate(int i);  
public class DelegateStart
{
        public void Start() 
        {
            //创建委托对象与赋值
            //delUpdate d = new delUpdate(test1);
            delUpdate d = test1;
            d += test2;
            if (d != null)
            {
                //调用委托
                d(55);
            }

            //使用Invoke和空条件运算符
            d?.Invoke(65);
        }
        public void test1(int i)
        { 
        }

        public void test2(int i)
        {
        }
}

调用带返回值的委托
如果委托有返回值并且在调用列表中有一个以上的方法,会发生下面的情况。
调用列表中最后一个方法返回的值就是委托调用返回的值。
调用列表中所有其他方法的返回值都会被忽略。

调用带引用参数(ref)的委托
如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。
在调用委托列表中的下一个方法时,参数的新值(不是初始值)会传给下一个方法。

事件

发布者和订阅者
很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。

发布者( publisher ) 发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
订阅者( subscriber) 注册并在事件发生时得到通知的类或结构。
事件处理程序( event handler) 由订阅者注册到事件的方法, 在发布者触发事件时执行。
事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
触发(raise)事件调用( invoke)或触发( fire) 事件的术语。当事件被触发时,所有注册到它的方法都会被依次调用。

public delegate void delUpdate(int i);     //委托定义,相当于一个函数签名,函数指针
    public class FaBu
    {
        public event delUpdate ENotify;    //定义事件,该事件引发此委托类型的事件处理函数

        public void FBgo() 
        {
            if (ENotify != null)
            {
                //调用事件
                ENotify(55);
            }

            //使用Invoke和空条件运算符
            ENotify?.Invoke(65);
        }
    }

    public class DinYue
    {
        public DinYue(FaBu faBu)
        {
            faBu.ENotify += DYgo;
        }
        public void DYgo(int i)
        {
            
        }
    }

接口

接口是指定一组函数成员而不实现它们的引用类型。所以只能类和结构来实现接口。

如果类实现了接口,它必须实现接口的所有成员。

类或结构可以实现任意数量的接口。
所有实现的接口必须列在基类列表中并以逗号分隔(如果有基类名称,则在其之后)。

is、as转换

is运算符只可以用于引用转换以及装箱和拆箱转换,不能用于用户自定义转换。

使用is运算符来检查转换是否会成功完成,从而避免盲目尝试转换。

as运算符和强制转换运算符类似,只是它不抛出异常。如果转换失败,它返回null而不是
抛出异常。

和is运算符类似,as 运算符只能用于引用转换和装箱转换,不能用于用户自定义转换或到
值类型的转换。

泛型

声明一个简单的泛型类和声明普通类差不多,区别如下。
在类名之后放置一组尖括号。
在尖括号中用逗号分隔的占位符字符串来表示需要提供的类型。这叫作类型参数(type
parameter )。
在泛型类声明的主体中使用类型参数来表示替代类型。

约束使用where子句列出。
每一个有约束的类型参数都有自己的where子句。
如果形参有多个约束,它们在where子句中使用逗号分隔。

类名 只有这个类型的类或从它派生的类才能用作类型实参
class 任何引用类型,包括类、数组、委托和接口都可以用作类型实参
struct 任何值类型都可以用作类型实参
接口名 只有这个接口或实现这个接口的类型才能用作类型实参
new() 任何带有无参公共构造函数的类型都可以用作类型实参。这叫作构造函数约束

    public class Box<T> where T : class
    {
        private T _item;

        public Box(T item)
        {
            _item = item;
        }

        public T Item
        {
            get { return _item; }
            set { _item = value; }
        }
    }

最多只能有一个主约束,而且必须放在第一位。
可以有任意多的接口名称约束。
如果存在构造函数约束,则必须放在最后。

和非泛型类一样,泛型类的扩展方法:
必须声明为static;
必须是静态类的成员;
第一个参数类型中必须有关键字this,后面是扩展的泛型类的名字。

协变
out,协变关系允许程度更高的派生类型处于返回及输出位置
允许你使用派生类替换基类

    interface IBar<out T>
    {
        T GetValue();
    }

    class Bar : IBar<string>
    {
        public string GetValue()
        {
            throw new NotImplementedException();
        }

    }
    
     IBar<string> baseStr = new Bar();
     IBar<object> baseObj = baseStr;

逆变
in,逆变允许更高程度的派生类型作为输入参数
允许你将基类替换为派生类。

    interface IBar<in T>
    {
        void Set(T value);
    }

    class Bar : IBar<object>
    {
        public void Set(object value)
        {
            throw new NotImplementedException();
        }
    }
    
    IBar<object> baseObj = new Bar(); 
    IBar<string> baseStr = baseObj;

可变性处理的是可以使用基类型替换派生类型的安全情况,反之亦然。
因此可变性只适用于引用类型,因为不能从值类型派生其他类型。
使用in和out关键字的显式变化只适用于委托和接口,不适用于类、结构和方法
不包括in和out关键字的委托和接口类型参数是不变的。这些类型参数不能用于协变或
逆变。

1、将可变成员作为方法的输入参数,则当前成员的泛型参数可变性必须与输入成员的泛型参数可变性相反。

2、将可变成员作为方法的返回参数,则当前成员的泛型参数可变性必须与输出成员的泛型参数可变性相同。

LINQ

查询表达式的结构

子句必须按照一定的顺序出现。

from子句和select. . .group子句这两部分是必需的。
其他子句是可选的。
在LINQ查询表达式中,select 子句在表达式最后。这与SQL的SELECT语句在查询的开
始处不一样。
可以有任意多的from…let… .where子句。

        public void Main()
        {
            var numbers = new int[] { 2, 6, 4, 8, 10 };
            int howMany = (from n in numbers where n < 7
                            select n) .Count();
            Console.WriteLine($"Count: { howMany }");
        }

join语句基本和SQL语句差不多,但不能用==,使用equals。
let子句接受一个表达式的运算并且把它赋值给一个需要在其他运算中使用的标识符。

查询延续: into 子句
查询延续子句可以接受查询的一部分的结果并赋予-个名字,从而可以在查询的另-部分中
使用。

        static void Main()
        {
            var groupA = new[] { 3, 4, 5, 6 };
            var groupB = new[] { 4, 5, 6, 7 };
            var someInts = from a in groupA
                           join b in groupB on a equals b
                           into groupAandB //查询延续
                           from C in groupAandB
                           select C;
            foreach (var v in someInts)
                Console.Write($"{ v } ");
        }

枚举器和迭代器

为什么数组可以使用foreach?

数组可以按需提供一个叫作枚举器( enumerator)的对象。
枚举器可以依次返回请求的数组中的元素。枚举器“知道”项的次序并且跟踪它在序列中的位置,
然后返回请求的当前项。

对于有枚举器的类型而言,必须有一种方法来获取它。获取对象枚举器的方法是调用对象的
GetEnumerator方法。实现GetEnumerator方法的类型叫作可枚举类型( enumerable type或enumerable )。
数组是可枚举类型。

foreach工作原理
foreach结构设计用来和可枚举类型一起使用。只要给它的遍历对象是可枚举类型,比如数
组,它就会执行如下行为:
1、通过调用GetEnumerator方法获取对象的枚举器;
2、从枚举器中请求每一项并且把它作为迭代变量( iteration variable ),代码可以读取该变量
但不可以改变。

IEnumerator接口
实现了IEnumerator接口的枚举器包含3个函数成员: Current、 MoveNext 以及Reset。
Current是返回序列中当前位置项的属性。
它是只读属性。
它返回object类型的引用,所以可以返回任何类型的对象。
MoveNext是把枚举器位置前进到集合中下一项的方法。它也返回布尔值,指示新的位置
是有效位置还是已经超过了序列的尾部。
如果新的位置是有效的,方法返回true。
如果新的位置是无效的(比如当前位置到达了尾部),方法返回false。
枚举器的原始位置在序列中的第一项之前,因此MoveNext必须在第一次使用Current之
前调用。
Reset是把位置重置为原始状态的方法。

//这段代码产生了如下的输出,与使用内嵌的foreach语句的结果-样
        static void Main1()
        {
            //创建数组
            int[] arr1 = { 10, 11, 12, 13 };

            //获取并存储枚举器
            IEnumerator ie = arr1.GetEnumerator();

            //移到下一项
            while (ie.MoveNext())
            {

                //获取当前项
                int item = (int)ie.Current;
                //输出
                Console.WriteLine($"Item value: { item }");
            }
        }


IEnumerable 接口
可枚举类是指实现了IEnumerable 接口的类。IEnumerable接口只有一一个成员— GetEnumerator
方法,它返回对象的枚举器。

异步编程

关于线程,需要了解以下知识点。
默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。
线程可以派生其他线程,因此在任意时刻,一个进程都可能包含不同状态的多个线程,
它们执行程序的不同部分。
如果一个进程拥有多个线程,它们将共享进程的资源。
系统为处理器执行所调度的单元是线程,不是进程。

async/ await特性的结构

调用方法( calling method):该方法调用异步方法,然后在异步方法执行其任务的时候继
续执行 (可能在相同的线程上,也可能在不同的线程上)。
**异步(async)**方法:该方法异步执行其工作,然后立即返回到调用方法。
await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任
意多个await表达式,不过如果一个都不包含的话编译器会发出警告。
Task:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是
Task。调用方法将通过读取Task的Result属性来获取这个T类型的值。

class Program
    {
        static void Main()
        {
            Task<int> value = DoAsyncStuff.CalculateSumAsynch(5, 6);
        }
        //调用方法
        static class DoAsyncStuff
        {
            //当在一个异步方法中使用await关键字时,该方法将暂停执行,并将控制权返回给调用方。
            //直到等待的异步操作完成,该方法才会继续执行。避免阻塞当前线程.
            public static async Task<int> CalculateSumAsynch(int i1, int i2)
            {
                int sum = await Task.Run(()=> i1 + i2);

                return sum;
            }
        }
    }


BackgroundWorker 类
async/await 特性更适合那些需要在后台完成的不相关的小任务。
但有时候,你可能需要另建一个线程, 在后台持续运行以完成某项工作,并不时地与主线程
通信。BackgroundWorker 类就是为此而生的。

类有3个事件,用于发送不同的程序事件和状态。
在后台线程开始的时候触发DoWork
在后台任务汇报进度的时候触发ProgressChanged事件。
在后台工作线程退出的时候触发RunlorkerCompleted事件。

3个方法
调用RunWorkerAsync方法获取后台线程并且执行DoWork事件处理程序。
调用CancelAsync方法把CancellationPending属性设置为true。DoWork 事件处理程序
需要检查这个属性来决定是否应该停止处理。
Dowork事件处理程序(在后台线程)在希望向主线程汇报进度的时候,调用ReportProgress
方法。

如果希望工作线程向主线程汇报进度,需要把WorkerReportsProgress属性设置为true。
如果希望从主线程取消工作线程,就把WorkerSupportsCancellation属性设置为true。
既然对象已经配置好了,就可以通过调用RunWorkerAsync 方法来启动它。这会获取一个
后台线程,触发DoWork事件并在后台执行事件处理程序。

BeginInvoke和EndInvoke

delegate long MyDel( int first, int second );    //委托声明
static long Sum(int x, int y){ return x + y; }    //方法匹配委托
MyDel del= new MyDel(Sum);  //创建委托对象
IAsyncResult iar = del.BeginInvoke( 3, 5, null, null ); //开始异步调用
long result = del. EndInvoke(iar); //等待结束获取结果

预处理指令

指示编译器如何处理源代码。

语法规则:
预处理指令必须和C#代码在不同的行。
与C#语句不同,预处理指令不需要以分号结尾。
包含预处理指令的每- -行必须以字符开始。

#define debug  // 声明一个编译符号,必须放在第1行
        static void Main1()
        {
#if debug
            //开启详细日志
#else
            //性能优化
#endif
        }

反射和特性

Type

对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象。
不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例。

你可以从type类中获取类型的几乎所有内容。

Name 属性 返回类型的名字
Namespace 属性 返回包含类型声明的命名空间
Assembly 属性 返回声明类型的程序集。如果类型是泛型的,返回定义这个类型的程序集
GetFields 方法 返回类型的字段列表
GetProperties 方法 返回类型的属性列表
GetMethods 方法 返回类型的方法列表

Type t = class.GetType();
还可以使用typeof运算符来获取Type对象。只需要提供类型名作为操作数
Type t = typeof ( class );

特性

可以使用Type对象的IsDefined方法来检测某个特性是否应用到了某个类上。
Type类的GetCustomAttributes方法返回应用到结构上的特性的数组。

特性( ttribute )是一种允许我们向程序的程序集添加元数据的语言结构。它是用于保存程
序结构信息的特殊类型的类。
1、将应用了特性的程序结构( program contruct )叫作目标( target )。,
2、设计用来获取和使用元数据的程序( 比如对象浏览器)叫作特性的消费者( consumer )。
3、.NET预定了很多特性,我们也可以声明自定义特性。

特性的目的是告诉编译器把程序结构的某组元数据嵌人程序集。可以通过把特性应用到结构
来实现。

Obsolete特性
你肯定希望团队成员使用新代码。要警告他们不要使用旧方法, 可以使用Obsolete特性将程序结构标注为“过时”,并且在代码编译时显示。

[Obsolete("使用方法过时!",true)]
static void TraceMessage(string str)
{ 
    Console.WriteLine(str);
}

//obsolete 特性将程序结构标注为过时 并且在代码编译时显示有用的警告信息
//true 被标记为错误 而不仅仅是警告

Conditional特性
允许我们包括或排斥特定方法的所有调用。

#define DoTrace
using System;
using System.Diagnostics;
namespace AttributesConditional
{
    class Program
    {
        [Conditional("DoTrace")]
        static void TraceMessage(string str)
        { 
            Console.WriteLine(str);
        }
        
        static void Main()
        {
            //若包括此方法的调用,则需要包含 程序第一行 #define dotrace 的定义
            //若排斥此方法的调用,则需要注释 程序第一行 #define dotrace 的定义
            TraceMessage("Start of Main");
            Console.WriteLine("Doing work in Main. ");
            TraceMessage("End of Main");
        }
    }
}

调用者信息特性
利用调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。
这3个特性名称为CallerFilePath、Callerl ineNumber和CallerMemberName。
这些特性只能用于方法中的可选参数。

    public static class Program
    {
        public static void MyTrace(string message,
        [CallerFilePath] string fileName = "",
        [CallerLineNumber] int lineNumber = 0,
        [CallerMemberName] string callingMember = "")
        {
            Console.WriteLine($"File:{ fileName }");
            Console.WriteLine($"Line:{ lineNumber }");
            Console.WriteLine($"Called From: { callingMember }");
            Console.WriteLine($"Message:{ message }");
        }
        public static void Main()
        {
            MyTrace("Simple message");
        }
    }

DebuggerStepThrough特性
调试不要进入某些方法

        [DebuggerStepThrough]
        static void TraceMessage(string str)
        { 
            Console.WriteLine(str);
        }
        
        // F11也进入不到这个方法的内部
        TraceMessage("Start of Main");
        

声明自定义特性
总体来说,声明一个特性类和声明其他类一样。
声明一个派生自System.Attribute的类。
给它起一个以后缀Attribute结尾的名字。
安全起见,通常建议声明一个sealed的特性类。
public sealed class MyAttributeAttribute : System.Attribute
由于特性持有目标的信息,所有特性类的公有成员只能是:字段、属性、构造函数

        //该特性限制自定义特性 只能作用于某个目标类型上上
        //AttributeUsage 特性有一个位置参数:AttributeTargets 两个可选参数:Inherited,AllowMultiple

        //MyAttributeAttribute 这个自定义特性 只能作用于 方法或者构造器上,
        //Inherited =false 不会被应用到它的类的派生类所继承,
        //AllowMultiple =false 不能在同一个目标上多次使用自定义特性 MyAttributeAttribute
        [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor, Inherited = false, AllowMultiple = false)]
        public class MyAttributeAttribute : System.Attribute
        {

        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值