CLR-C#概述

前言

         目前正在学NET CLR viaC#(第四版),将所学到的东西与自己搜集到的资料整理汇总概括成该篇文章,以供自己和大家参考。

        文章持续更新中。。。。更新速度等同与我的学习速度。。。

 

第一章-clr的执行模型

CLR的执行模型:

     1.将源代码编译成托管模块

什么是托管模块?托管模块是由PE(Portable Executable,可以指执行体)头,CLR头,元数据,IL代码组成。

什么是IL代码?IL代码也称托管代码,是一种类似汇编语言的“中间语言”,可以将其理解为伪汇编语言。

 

     2.将托管模块合并成程序集

什么是程序集?程序集是一个或多个(托管)模块/资源文件的逻辑性分组,CLR实际不直接和托管模块工作,实际是和程序集工作,程序集是重用,安全性以及版本控制的最小单元

     3.加载公共语言运行时

     4.执行程序集代码

为了执行程序集的代码,首先需要把方法的IL代码转换为本机CPU指令,这是CLR的JIT编译器的职责。

可知:

1.方法仅在第一次被调用时才有一些性能损失。以后的调用都以本机代码的形式全速运行。

2.JIT编译器将得到的将本机CPU指令保存在动态内存中。也就是说,一旦应用程序关闭,编译好的代码也会丢失。也就是说,再次运行应用程序时JIT编译器需要把IL代码再次编译为本机CPU指令。

托管代码和非托管代码的区别?

1.什么是非托管代码?非托管代码,直接编译成目标机器码。不在CLR上运行(也就是没有编译成IL代码),在公共语言运行库环境的外部,由操作系统直接执行的代码,代码必须自己提供垃圾回收,类型检查,安全支持等服务(如需要内存管理等服务,必须显示调用操作系统的接口,通常调用Windows SDK所提供的API来实现内存管理)。非托管代码的运行速度比托管代码运行速度快。

2.两者区别:1、托管代码是一种中间语言,运行在CLR上;

非托管代码被编译为机器码,运行在机器上。2、托管代码独立于平台和语言,能更好的实现不同语言平台之间的兼容;非托管代码依赖于平台和语言。3、托管代码可享受CLR提供的服务(如安全检测、垃圾回收等),不需要自己完成这些操作;非托管代码需要自己提供安全检测、垃圾回收等操作。

 

使用托管代码的优点:

1,托管代码会将所有Windows进程放入独立的地址空间,将获得健壮性和稳定性,一个进程干扰不到另一个进程;

2,但如果进程数量太多,会损害性能并制约可用的资源,而托管代码可以用一个进程运行多个应用程序,以此减少进程数,从而增强性能,减少所需的资源

 

公共语言规范(CLS)

CLS定义了所有语言都必须支持的最小功能集。

CTS:通用类型系统(Common Type System)

第二章-生成、打包、部署和管理应用程序及类型

什么是元数据?

元数据是描述数据的数据。元数据是一个二进制数据块,由几个表组成。这些表分为三个类别:定义表、引用表和清单表。

第三章-程序集

CLR支持的程序集类型。

弱命名程序集(weakly named assembly)和强命名程序集(strongly named assembly)。二者的区别:强命名程序集使用发布者的公钥/私钥进行了签名。这一堆密钥允许对程序集进行唯一性的标识、保护和版本控制。

强命名程序集既可以私有部署,也可以全局部署,弱命名程序集只能进行私有部署。

强命名程序集防能篡改

第四章-类型基础

所有类型都从System.Object派生。

System.Object的Public方法:

1.Equals:如果两个对象具有相同的值就返回true(对于Object的Equals方法的默认实现:它实现的实际是同一性,而非相等性)

2.GetHashCode:返回对象的哈希码(如果某个类型的对象要在哈希表集合中作为键使用,类型应重写该方法)。

3.ToString:默认返回类型的完整名称(this.GetType().FullName)。但经常重写该方法来返回包含对象状态表示的String对象(例如Boolean和Int32等核心类型重写ToString方法来返回它们的值的字符串表示)

4.GetType:返回从Type派生的一个类型的实例,指出调用GetType的那个对象的类型。返回的Type对象可以和反射类型配合。获取与对象的类型有关的元数据信息。

PS:需要注意的是: 我们在重写Equals时,GetHashCode也要一起重写;

因为有一个不重写就会调用基类的方法,依然会根据地址判断;

eg: Equals(Enumerable,Enumerable),重写Equals,遍历内容判断是否相同

System.Object的Protect方法:

1.MemberwiseClone:这个非虚方法创建类型的新实例,并将新对象的实例字段设置与this对象的实例字段完全一致。返回对新实例的引用。(浅拷贝)

2.Finalize:在垃圾回收器判断对象应该作为垃圾被回收后,在对象的内存被实际回收之前,会调用这个虚方法。(需要在回收内存前执行清理工作的类型应重写这个方法)

用new操作符创建一个对象时会发生什么:

1.计算类型及其所有基类中定义的所有实例字段需要的字节数。(堆上每个对象都需要一些额外的成员——开销成员,包括“类型对象指针”和“同步块索引”,CLR利用这些成员管理对象,额外成员的字节数要计入对象大小)

2.从托管堆(项目脚本运行时的内存管理器自动管理的一段内存。)中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0

3.初始化对象的“类型对象指针”和“同步块索引”成员

4.调用类型的实例构造器,传递在new调用中指定的实参。(并且会自动调用基类的构造器直到最终调用System.Object的构造器)

5.返回指向新建对象的引用(指针)给栈上。

使用is和as操作符进行转型:

is操作符:is检查对象是否兼容于指定类型,然后进行转型,返回true或者false

as操作符:as检查对象是否兼容与指定类型,返回同一个对象的非null引用或者null

上面这段代码使用as操作符时,CLR只进行一次检查,只核实o是否兼容Employee类型

强制转换和as操作符速度比较,强制转换的速度慢于as操作符

程序集和命名空间

通过命名空间方便地定位类型

对于命名空间以及类型名字都相同的不同类型,可以使用“外部类型“来解决这类问题。

什么是类型对象指针和同步块索引?

类型对象:类型对象(Type Object)是在.NET平台中用于表示和描述一个类型的对象。每个在.NET中定义的类型都有一个对应的类型对象。

类型对象指针:在.NET平台中,每个类型都有一个对应的类型对象(Type Object),它包含了关于该类型的元数据信息,例如类型的方法、字段、属性等。类型对象指针是指向类型对象的指针,它指向类型对象在内存中的位置。通过类型对象指针,运行时系统可以访问和操作类型对象的元数据信息。类型对象指针在.NET中被用于实现反射、类型检查和动态代码生成等功能。

同步块索引:在多线程环境下,为了确保对象的线程安全性,.NET平台使用同步块(Sync Block)来实现对象的互斥访问。每个对象都有一个同步块,用于控制对该对象的并发访问。同步块索引是一个整数值,它表示对象的同步块在同步块表中的索引位置。同步块表是一个全局的数据结构,用于存储所有对象的同步块信息。通过同步块索引,运行时系统可以快速定位和操作对象的同步块,实现线程同步和互斥访问。

第五章-基元类型

基元类型的定义:编译器直接支持的数据类型。

以下是C#基元类型和对应的FCL类型

 

引用类型和值类型的区别

存储方式:值类型的实例直接存储在栈上,而引用类型的实例存储在堆上,并通过引用(指针)来访问。

复制行为:当值类型被赋值给另一个变量或作为参数传递时,会进行值的复制。而引用类型的复制只会复制引用,两个引用指向同一个对象。

空值:值类型不能为null,它们总是有一个默认值。而引用类型可以为null,表示不引用任何对象。

默认初始化:值类型在声明时会自动进行默认初始化,即赋予一个默认值,默认初始化为0。引用类型在声明时默认为null,且引用类型实例会有同步块索引和类型对象指针,值类型并没有。

内存管理:值类型的内存管理由编译器自动处理,它们的生命周期与其所在的作用域相关。引用类型的内存管理由垃圾回收器(Garbage Collector)负责,它们的生命周期不仅与作用域相关,还受到垃圾回收器的影响。

比较行为:值类型的比较是按值进行的,即比较它们的实际值是否相等。引用类型的比较默认是比较引用是否指向同一个对象,除非重写了Equals方法。

继承和多态:引用类型支持继承和多态,可以通过基类引用来引用派生类的实例。值类型不支持继承和多态。

 

Ps:

1.C#中的一些类型(如字符串string)看起来像引用类型,但实际上是特殊的引用类型,称为不可变引用类型。它们的行为类似于值类型,但在内部仍然使用引用来管理对象的生命周期。

2.值类型从System.ValueType派生,引用类型从System.Object派生,但System.ValueType也是从System.Objcet派生。另外枚举作为值类型,但是从System.Enum抽象类型派生。

同一性和相等性:

同一性判断地址是否相同,相等性判断数据是否相同。

值类型实例装箱时发生的事情:

1.在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,外加同步块索引和类型对象指针所需的内存量。

2.将值类型字段复制到新分配的堆内存。

3.返回对象的地址到栈上空间。

与已装箱的值类型实例拆箱相关的事情:

1.如果包含对“已装箱的值类型实例的引用”的变量为null,抛出NullReferenceException异常

2.如果引用的对象不是所需值类型的已装箱实例,抛出InvalidCastException异常。

3.拆箱和隐式转换不可以同时进行,必须把这个过程拆开变成显示转换。

4.已装箱的值类型实例,拆箱时会伴随一次复制(把这些字段从堆复制到栈上的变量中去),因此修改堆中已装箱的值类型必须先拆箱在栈上修改再装箱(见《CLR via C#》P113)

5.即使String作为特殊的引用类型会存储在栈上,但是值类型转化为String也需要装箱(见《CLR via C#》P114~115装箱3次代码)

6. 拆箱只是把引用类型中的字段复制到栈上的一个临时变量中

Dynamic和var的异同:

相同点:

类型推断:dynamic和var都是用于进行类型推断,即由编译器根据变量的初始化值来确定变量的类型。

运行时解析:dynamic和var都允许在运行时进行类型解析和动态绑定。它们可以在编译时不确定变量的具体类型,而是在运行时根据实际情况进行类型检查和调用。

不同点:

静态类型 vs 动态类型:var声明的变量是静态类型,即在编译时确定变量的类型,并且变量的类型在初始化时被确定。而dynamic声明的变量是动态类型,即在编译时不确定变量的类型,而是在运行时根据实际情况确定变量的类型。

编译时检查 vs 运行时检查:var声明的变量在编译时会进行类型检查,如果变量的初始化值与推断的类型不匹配,会导致编译错误。而dynamic声明的变量在编译时不进行类型检查,类型检查和绑定是在运行时进行的,因此可以接受任何类型的值。

静态绑定 vs 动态绑定:var声明的变量在编译时进行静态绑定,即变量的类型在编译时确定,绑定的方法和成员也在编译时确定。而dynamic声明的变量在运行时进行动态绑定,即变量的类型和绑定的方法和成员在运行时根据实际情况确定。(即dynamic是运行时绑定)

Ps:需要注意的是,使用dynamic会带来一些运行时的性能开销,因为类型检查和绑定是在运行时进行的。而var则没有这个开销,因为类型检查和绑定是在编译时确定的。因此只是一两个位置需要动态行为,调用反射方法更好。

Dynamic只能访问对象的实例成员,因为dynamic必须引用对象。但有时动态调用运行时才能确定一个类型的静态成员。

第六章-类型和成员基础

类型(Class)的各种成员:

1.常量(const)。常量总与类型关联,不与类型的实例关联。常量在逻辑上总是静态成员。

2.字段。静态字段被认为是类型状态,实例字段被认为是对象状态。

3.实例构造器。

4.类型构造器。

5.方法。方法是更改或查询类型或对象状态的函数,作用于类型称为静态方法,作用于对象称为实例方法。

6.操作符重载。操作符重载实际是方法,定义了当操作符作用于对象时,应该如何操作该对象。不是CLS(公共语言规范)的一部分。

7.转换操作符。转换操作符是定义如何隐式或显式将对象从一种类型转型为另一种类型的方法。不是CLS的一部分。

8.属性。属性允许用简单的字段风格的语法设置或查询类型或对象的逻辑状态,同时保证状态不被破坏。作用于类型称为静态属性,作用于对象称为实例属性,属性可以无参也可以有多个参数。

9.事件。静态事件允许一个类型向一个或多个静态或实例方法发送通知;实例事件允许对象向一个或多个静态或实例方法发送通知。引发事件通常是为了响应提供事件的类型或对象的状态的改变。

10.类型。类型可以定义其他嵌套类型。通常用这个办法将大的、复杂的类型分解成更小的构建单元以简化实现。

类型的可见性

静态类

C#编译器对静态类的限制:

1.静态类必须直接从Object类派生。继承只适用于类型对象,静态类无法实例化。

2.静态类不能实现任何接口。因为只要使用类的实例才能调用类的接口方法。

3.静态类只能定义静态成员。

4.静态类不能作为字段、方法参数或者局部变量使用,因为这些都代表引用了实例的变量。

分部类、结构和接口

1.Partial关键字。

2.类、结构或接口的定义源代码可以分散到一个或多个源代码文件中。

3.分部类功能完全由C#编译器实现,CLR对该功能一无所知(一个类型的所有源代码文件必须作为一个编译单元编译到一起)

使用分部类的好处:

1.源代码控制。Partial关键字可将类型的代码分散到多个源代码文件中,每个文件都可以单独签出,多个程序员可以同时编辑该类型。

2.在同一个文件中将类或结构分解成不同的逻辑单元。

3.代码拆分。

第七章-常量与字段

常量const

常量是值从不变化的符号

在C#中,基元类型都可以用于定义常量。同时,非基元类型也允许被定义为常量变量,但前提是,把值设为null。

字段

字段是一种数据成员,其中容纳了一个值类型的实例或则一个引用类型的引用。

字段有以下几种修饰符:

Ps:readonly除了可以在构造器方法写入,还可以使用反射修改。

readonly不可改变的是引用而非字段引用的对象。

静态字段在生成类型对象时分配空间,实例字段在生成实例时分配空间。

 

第八章 方法

实例构造器和类(class)(引用类型)

实例构造器和结构(struct结构体)(值类型)

构造器是将类型的实例初始化为良好状态的特殊方法

设置类型实例的初始状态

构造器(Constructor)就是构造方法(Constructor Method)

类型构造器(静态构造器)

设置类型的初始状态

和实例构造器的区别:构造方法前面加static

权限总是为private

操作符重载

第九章 参数

可选参数:

方法、构造函数、索引器或委托的定义可以指定其形参为必需还是可选。任何调用都必须为所有必需的形参提供实参,但可以为可选的形参省略实参。每个可选形参都有一个默认值作为其定义的一部分。如果没有为该形参发送实参,则使用默认值。

命名参数:

通过命名实参,不再需要将实参的顺序与所调用方法的形参列表中的形参顺序相匹配。每个形参的实参都可按形参名称进行指定。

隐式类型局部变量(var):

Var存在的原因:C#能根据初始化表达式的类型判断方法中的局部变量的类型。

 

Var存在的意义:隐式局部变量 var 的存在意义在于它可以让编译器从初始化表达式推断出变量的类型。这样,开发人员就不必考虑变量的具体类型,可以提高开发效率。例如有一个表达式 var x = new List<int>(),则编译器会自动推断出 x 的类型为 List<int>。

 

使用var需要注意的几点(其他的见第五章笔记中dynamic和var的对比):

1.var只能用于方法内部的局部变量。不能用于声明方法的参数类型以及返回值类型等。

2.var不能用于声明静态字段。

3.不能将null赋值给var局部变量。

以传引用的方式向方法传递参数

CLR允许以传引用的的方式传递参数,这在C#中体现在ref和out这两个关键字。并且CLR不区分ref和out关键字,无论使用哪个关键字都生成相同的IL代码。

in 关键字  外部初始化且不允许修改

Ref关键字,ref必须在该函数外部进行初始化(在调用该函数前必须初始化了这个变量)。

Out关键字,out必须在该函数内部进行初始化(在函数内必须有该变量的初始化语句)。

确实使用ref和out生成的IL代码都是一样的,即元数据签名一样(正因如此重载中ref和out是“完全一样的”)

Ps:为大的值类型使用out或者ref可以提升代码的执行效率,因为避免了在进行方法调用时复制值类型实例,ref和out效率相当。

仅当方法“返回”对“方法知道的一个对象”的引用时,为引用类型使用out和ref才有意义。(这句话怎么理解P194)

 

当方法返回时,调用方法时传递给该方法的变量将引用一个新的对象,即改变对象的引用,这样使用ref和out关键字传递引用类型参数才有意义。

向方法传递可变数量的参数(params)

变长参数的例子:

Params关键字后接一个数组作为变长参数,且params关键字仅能用于方法签名中的最后一个参数,且params只能标识一维数组,即变长参数只能是一维数组,而且可以为变长参数数组传递null。

eg:

参数和返回类型的设计规范:

参数类型最好为弱类型:灵活,返回类型最好为强类型:安全性和可靠性

 

Ps:什么是强参数类型,什么是弱参数类型。

强参数类型是指在定义函数或方法时,参数被明确指定了一个数据类型,并且在编译时会进行类型检查。

弱参数类型则是指在定义函数或方法时,参数的类型较为宽松或灵活,允许在运行时进行隐式类型转换。

eg:接口和int

常量性

什么是常量性?

常量性是指对象的某些属性或方法在对象的整个生命周期内保持不变。在 C++ 中,可以使用 const 关键字来声明常量数据成员和常量成员函数。常量数据成员在对象创建后不能被修改,而常量成员函数不能修改对象的任何非静态数据成员。这样可以确保对象的某些属性在整个生命周期内保持不变,从而提高代码的可靠性和可维护性。

 

CLR不支持常量性,原因是,如果支持常量性CLR就要对每个写入操作进行验证以确定更改的不是常量,这对性能影响很大。且在违反常量性的地方会造成CLR抛出异常,且会给开发人员带来大量复杂性。

第十章 属性

属性(Property)

C# 中的属性(Property)是类和结构体中用于封装数据的成员。它们提供了一种方式来定义类成员的访问和设置规则,通常用于隐藏字段(Fields)的内部实现细节,同时提供控制数据访问的机制。

属性可以看作是对字段的包装器,通常由 get 和 set 访问器组成。

属性(Property)不会确定存储位置。相反,它们具有可读写或计算它们值的 访问器(accessors)

例如,有一个名为 Student 的类,带有 age、name 和 code 的私有域。我们不能在类的范围以外直接访问这些域,但是我们可以拥有访问这些私有域的属性。

基本语法

public class Person
{
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

以上代码中,Name 属性封装了私有字段 nameget 访问器用于获取字段值,而 set 访问器用于设置字段值。

自动实现的属性

如果你只需要一个简单的属性,C# 允许使用自动实现的属性,这样你不需要显式地定义字段。

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

在这种情况下,编译器会自动为 Name 属性生成一个私有的匿名字段来存储值。

对象和集合初始化器

存在的意义:可以在构造一个对象的同时设置对象的一些public属性或字段。

例子:

匿名类型

匿名类型以及初始化的例子,书P209

C#编译器会通过等号右边的表达式推测类型(可以类比隐式类型局部变量var)

 

编译器支持用另外两种语法声明匿名函数中的属性,能根据变量推断属性名和类型:

Ps:如果源代码中定义了多个匿名类型,而且这些类型具有相同的结构,那么只会创建一个匿名类型定义,但创建该类型的多个实例。

 

方法不能返回对匿名类型的引用(匿名类型在编译时会被赋予一个临时名称,但我们并不能获得这个每次编译都发生改变的名称)。

System.Tuple类型

有参属性

有参属性即索引器,它的get访问器方法接收一个或多个参数,set访问器方法接收两个或多个参数

定义有参属性需要在类型后加上this,后面的方括号“[ ]”内为参数。C#中索引器没有自己的名字,编译器生成代码时也不会按照名字区分索引器

索引器的例子:

本例中value为一个隐藏参数,代表想赋给“被索引元素”的新值或键值对中的值,方括号中是索引或者键值对中的键

调用属性访问器方法时的性能

对于简单的get和set访问器方法(例如AIP的get和set),JIT编译器会将代码内联,因此这种情况下使用属性不会造成性能上的损失(而属性如果执行了其他的逻辑就会造成性能的损失)。

属性访问器的可访问性

定义属性时如果两个访问器方法需要不同的访问性,C#要求必须为属性本身限制最小的访问性,两个访问器 只能选择一个来使用较大的(另一个只能使用和属性本身一样的修饰符)。

例子:

Set方法使用了限制更大的private,则get方法只能使用和属性本身一样的public

第11章 事件

定义一个事件:使用event关键字

本章主要靠实践

第十二章 泛型

开放类型和封闭类型

具有泛型类型参数的类称为“开放类型”;为所有类型参数都传递了实际的数据类型,该类型即“封闭类型”。

CLR是如何解决代码爆炸的问题的:

CLR认为所有引用类型实参完全相同(所以生成的本机代码是相同的)。

逆变和协变

协变:协变允许将一个泛型类型的实例赋值给另一个泛型类型,只要它们的类型实参具有继承关系。(实例是子类) out 

逆变:允许将一个泛型类型的实例赋值给另一个泛型类型,只要它们的类型实参具有相反的继承关系。(实例是父类)in

 

逆变用于传入的参数值,协变用于返回值(原理是里氏替换原则)

逆变用于输入参数,因为它允许您使用更具体的类型来替换方法签名中输入参数的更一般类型。例如,假设您有一个接受 Animal 类型参数的方法,您可以使用 Cat 类型(Animal 的子类)来替换该参数,因为 Cat 是 Animal 的一种更具体的类型。

协变用于返回值,因为它允许您使用更一般的类型来替换方法签名中返回值的更具体类型。例如,假设您有一个返回 Cat 类型值的方法,您可以使用 Animal 类型(Cat 的基类)来替换该返回值,因为 Animal 是 Cat 的一种更一般的类型。

 

协变的例子:

协变的关键字为out,加在尖括号内的T之前

 

逆变的例子:

协变的关键字为in,加在尖括号内的T之前

泛型约束

 

 where T : class:类型参数必须是引用类型。

  where T : struct:类型参数必须是值类型。

  where T : new():类型参数必须有一个无参数的构造函数。

  where T : BaseClass:类型参数必须是 BaseClass 或其派生类。

  where T : Interface:类型参数必须实现特定的接口。

  where T : U:类型参数 T 必须与另一个类型参数 U 兼容,通常用于协变或逆变场景。

 

第十三章 接口

如何定义一个接口:

使用interface关键字,然后指定名称和实例方法的签名,接口里的方法不可以是静态的不可以有修饰符

例子:

Ps:接口使用大写的“i”开头便于辨别

接口是“缩水版”的多继承,C#不支持多继承,通过接口提供类似的方案。CLR只允许显示实现接口,而C#语言允许在只继承一个接口时使用隐式实现,而继承多个接口时必须显示实现每一个方法。

能使用基类实例的地方一定能使用其派生类(因为派生类继承了基类的所有特征,也就是“多态”),接口类型和实现了该接口类型的类之间的关系也是如此

下面是例子:

 

CLR认为接口定义就是类型定义,同样会为接口类型定义内部数据结构(类型对象指针和同步块索引)

接口无法继承类,只可以继承接口

第十四章 字符、字符串和文本处理

.Net Framework中字符类型(System.Char,值类型)都为16位的Unicode类型字符。

如何将Char与其他数值类型相互转换?

1.强制转换

2.System.Convert类提供的一系列静态方法(异常会抛出OverflowException)

3.IConvertible接口。因为Char类型和FCL的所有数值类型都实现了IConvertible接口,该接口定义了ToUInt16和ToChar等类似的方法(这些方法是显式实现的),通过给值类型装箱的方式转换为IConvertible类型然后显式调用这些方法返回一个拆箱后的对应值类型实例。

eg:

System.String

特性:

1.String是引用类型,直接派生于Object类型,它的实例存在于堆上;而Char为值类型,它的实例存在于线程栈上。但String被视为基元类型,即String可以直接使用字面值。

2.不允许使用new操作符构造String对象,必须使用简化语法。

3.不要对string类型字段使用字符串拼接,因为在运行时会消耗更多的性能,因为string为引用类型变量存储在堆上,而堆会进行垃圾回收释放内存,这么做会增加gc(垃圾收集(Garbage Collection))的频率因此影响性能。

 

“逐字字符串”声明字符串:

在引号前加@字符,使所有的字符都是字符串的一部分

例如

在字符串中含有/n这类转义字符可以避免歧义,且这么做代码的可读性会更好。

String对象不可变

 

1.可以在一个字符串上执行任何操作但是不改变字符串本身(会返回一个新的字符串)。

2.操纵或访问字符串时不会发生线程问题。

C#提供的判断字符串相等性或排序的方法

什么是区域性敏感排序?

使用区域性敏感排序和当前区域比较字符串是什么意思?

 

使用区域性敏感排序和固定区域比较字符串是什么意思?

Ps:尽量不要使用==或者!=进行字符串比较而应该用Equals,因为无法从操作符看出比较的方式(无从得知是默认比较还是区域性敏感比较或者其他)

如何执行语言文化的比较,P288例子

如果使用的是“默认比较”,那么“β”会自动被展开成ss。也就是说本例中第二行输出,无论使用默认比较还是区域性敏感比较,其结果都是“==”。在使用String的CompareTo方法时,如果没有指定语言文化就会调用线程的CurrentCulture属性值。

什么是字符串留用?

为什么需要字符串留用?

因为String具有不可变的性质,所以复制同一个字符串的多个实例是极其消耗堆空间的,而只保留字符串的一个实例可以显著提升内存利用率。

高效构造字符串

StringBulider和String的不同点:

String:String 类的对象是不可变的,这意味着一旦创建了一个 String 对象,它的值就不能被更改。如果需要对一个字符串进行修改,例如添加、删除或替换字符,那么需要创建一个新的 String 对象来存储修改后的字符串。这样做可能会导致大量的内存分配和垃圾回收,从而影响程序的性能 。

StringBuilder:StringBuilder 类提供了一种可变的字符串缓冲区,可以更有效地处理字符串。当需要对一个字符串进行频繁的修改时,可以使用 StringBuilder 类来避免不必要的内存分配和垃圾回收。StringBuilder 类提供了许多用于修改字符串的方法,例如 append()、insert() 和 delete() 等。当完成了对字符串的修改后,可以使用 toString() 方法将 StringBuilder 对象转换为一个 String 对象 。

Ps:应把StringBuilder视为特殊的String类型构造器。

StringBuilder如何实现可变字符串?

StringBuilder对象包含一个引用了Char结构构成的数组,然后StringBuilder的成员函数可以操控这个字符数组。如果字符串变大超过了原本数组的大小,StringBuilder会自动分配一个为原来大小2倍的新数组,然后把原数组中的元素复制到新数组中去,原数组倍垃圾回收。

 

StringBuilder独享的构造方式:

StringBuilder的相关概念:

1.StringBuilder的最大容量为Int32.MaxValue,为2的31次方-1

2.StringBuilder维护的字符数组的默认容量为16(在构造时没有设置容量)

3.字符中容量值和长度是不同的,Length是长度,而Capacity为容量,Length一定小于等于Capacity

ToString方法

Object类的ToString方法返回一个对象所属的类型的全名。如果想要一些其他的值,那么需要重写ToString方法。

如何实现指定语言文化和格式的ToString:

继承System.IFormattable接口

FormatProvider是实现了该接口的一个类型的实例

ToString 方法接受一个 format 参数,该参数指定要使用的格式。在 ToString 方法的实现中,可以使用条件语句(例如 if 或 switch)来检查 format 参数的值,并根据不同的格式执行相应的操作。

eg:

后续:

该章后面繁琐难懂用的少,日后再看

第十五章 枚举类型和位标志

枚举类型:枚举类型是一种值类型,它定义了一组常量字段和一个实例字段,这些常量可以表示一组离散的值。枚举类型通常用于定义一组相关的常量,例如星期几、月份或颜色。(被视为常量即会内嵌入程序集,即编译时需要枚举类型的程序集而运行时不需要枚举类型的程序集)

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值