您的位置:首页 > 编程语言 > C#

《C#高级编程》读书笔记

2016-03-03 18:50 225 查看
本文已同步发表在博客园:http://www.cnblogs.com/davidsheh/p/5236686.html

C#类型的取值范围



访问限制符

修饰符应用于说明
public所有类型或成员任何代码均可以访问该项
protected类型和内嵌类型的所有成员只有派生的类型能够访问该项
internal所有类型或成员只能在包含它的程序集中访问该项
private类型和内嵌类型的所有成员只能在它所属的类型中访问该项
protected internal类型和内嵌类型的所有成员只能在包含它的程序集和派生类型的任何代码中访问该项
C#常见的修饰符

修饰符应用于说明
new函数成员成员用相同的签名隐藏继承的成员
static所有成员成员不作用于类的具体实例
virtual仅函数成员成员可以由派生类重写
abstract仅函数成员虚拟成员定义了成员的签名,但没有提供实现代码
override仅函数成员成员重写了继承的虚拟或抽象成员
sealed类、方法和属性对于类,不能继承自密封类。对于属性和方法,成员重写已继承的虚拟成员,但任何派生类中的任何成员都不能重写该成员。该修饰符必须与override一起使用
extern仅静态[DllImport]方法成员在外部用另一种语言实现
结构体

结构是值类型,不是引用类型。

存储在栈中或存储为内联(inline)(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。

结构体不支持继承。

对于结构构造函数的工作方式有一些区别。尤其是编译器总是提供一个无参数的默认构造函数,它是不允许替换的。

使用结构,可以指定字段如何在内存中的布局。

注意,因为结构是值类型,所以
new
运算符与类和其他引用类型的工作方式不同。
new
运算符并不分配堆中的内存,而是只调用相应的构造函数,根据传送给它的参数初始化所有的字段。

结构遵循其他数据类型都遵循的规则:在使用前所有的元素都必须进行初始化。在结构上调用
new
运算符,或者给所有的字段分别赋值,结构就完全初始化了。当然,如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0。

结构是会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。正面的影响是为结构分配内存时,速度非常快,因为它们将内联或者保存在栈中。在结构超出了作用域被删除时,速度也很快。负面影响是,只要把结构作为参数来传递或者把一个结构赋予另一个结构(如A=B,其 中A和B是结构),结构的所有内容就被复制,而对于类,则只复制引用。这样就会有性能损失,根据结构的大小,性能损失也不同。注意,结构主要用于小的数据结构。但当把结构作为参数传递给方法时,应把它作为ref参数传递,以避免性能损失————此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值。

结构不是为继承设计的。这意味着:它不能从一个结构中继承。唯一的例外是对应的结构(和C#中的其他类型一样)最终派生于类
System.Object
。因此,结构也可以访问
System.Object
的方法。在结构中,甚至可以重写
System.Object
中的方法————如重写
ToString()
方法。结构的继承链是:每个结构派生自
System.ValueType
类,
System.ValueType
类又派生自
System.Object
ValueType
并没有给
Object
添加任何新成员,但提供了一些更适合结构的实现方式。注意,不能为结构提供其他基类,每个结构都派生自
ValueType


为结构定义构造函数的方式与为类定义构造函数的方式相同,但 不允许定义无参数的构造函数。默认构造函数把数值字段都初始化为0,把引用类型字段初始化为
null
,且总是隐式地给出,即使提供了其他带参数的构造函数,也是如此。提供字段的初始值也不能绕过默认构造函数。

扩展方法

扩展方法允许改变一个类,但不需要该类的源代码。所以使用扩展方法的情景之一是,当不知道类的源码或者不想修改该类的源码却想扩展该类,就可以用扩展方法。

扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中。

扩展方法需放在静态类中。

对于扩展方法,第一个参数是要扩展的类型,它放在
this
关键字的后面。

在扩展方法中,可以访问所扩展类型的所有共有方法和属性。

如果扩展方法与类中的某个方法同名,就从来不会调用扩展方法。类中已有的任何实例方法优先。

var
关键字。编译器可以根据变量的初始化值“推断 ” 变量的类型。使用var关键字需要遵循的一些规则:

变量必须初始化。否则,编译器就没有推断变量类型的依据。

初始化器不能为空。

初始化器必须放在表达式中。

不能把初始化器设置为一个对象,除非在初始化器中创建了一个新对象。

密封类和密封方法

C#允许把类和方法声明为
sealed
。对于类,这表示不能继承该类;对于方法,这表示不能重写该方法。

在把类或方法标记为
sealed
时,最可能的情形是:如果要对库、类或自己编写的其他类作用域之外的类或方法进行操作,则重写某些功能会导致代码混乱。也可以因商业原因把类或方法标记为
sealed
,以防第三方以违反授权协议的方式扩展该类。但一般情况下,在把类或成员标记为
sealed
时要小心,因为这么做会严重限制它的使用方式。即使认为它不能对继承自一个类或重写类的某个成员发挥作用,仍有可能在将来的某个时刻,有人会遇到我们没有预料到的情形,此时这么做就很有用。.Net基类库大量使用了密封类 ,使希望从这些类中派生出自己的类的第三方开发人员无法访问这些类。例如,
string
就是一个密封类。

约束

泛型支持的几种约束类型:

约束说明
where T : struct对于结构约束,类型T必须是值类型
where T : class类约束指定类型T必须是引用类型
where T : IFoo指定类型T必须实现接口IFoo
where T : Foo指定类型T必须派生自基类Foo
where T : new()这是一个构造函数约束,指定类型T必须有一个默认构造函数
where T1 : T2这个约束也可以指定类型T1派生自泛型类型T2。该约束也称为裸类型约束
只能为默认构造函数定义构造函数约束,不能为其他构造函数定义构造函数约束。

在C#中,
where
子句的一个重要限制是,不能定义必须由泛型类型实现的运算符。运算符不能再借口中定义。在
where
子句中,只能定义基类、接口和默认构造函数。

复制数组

如果数组的元素是值类型,调用Clone()方法就会复制所有值。如,int[] intArray1 = {1, 2}; int[] intArray2 = (int[])intArray1.Clone();其中intArray2数组的元素也变成了{1, 2}

如果数组包含引用类型,则不复制元素,而只复制引用。

除了使用Clone()方法之外,还可以使用Array.Copy()方法创建浅表副本。

Clone()方法和Copy()方法有一个重要区别:Clone()方法会创建一个新数组,而Copy()方法必须传递阶数相同且有足够元素的已有数组。

如果需要包含引用类型的数组的深层副本,就必须迭代数组并创建新对象。

Array
类使用
QuickSort
算法对数组中的元素进行排序。
Array
类中的Sort()方法需要数组中的元素实现
IComparable
接口。简单类型(如System.String和System.Int32)已经实现了IComparable接口。

元组

数组合并了相同类型的对象,而元组合并了不同类型的对象。

.NET 4
定义了8个泛型
Tuple
类和一个静态
Tuple
类,不同泛型
Tuple
类支持不同数量的元素。例如,
Tuple<T1>
包含一个元素,
Tuple<T1, T2>
包含两个元素,以此类推。

代码示例:

public class TupleExample
{
static void Main()
{
TupleExample example = new TupleExample();
var result = example.Divide(5, 2);
Console.WriteLine("result of division: {0}, reminder: {1}", result.Item1, result.Item2);
}

public static Tuple< int, int > Divide(int dividend, int divisor)
{
int result = dividend / divisor;
int reminder = dividend % divisor;

return TupleExample.Create<int, int>(result, reminder);
}
}


如果元组包含项超过8个,就可以使用带8个参数的Tuple类定义。最后一个模板参数是TRest,表示必须给它传递一个元组,这样就可以创建带任意个参数的元组了。示例:

var tuple = Tuple.Create<string, string, string, int, int, int, double, Tuple<int, int>>("Stephanie", "Alina", "Nagel", 2009, 6, 2, 1.37, Tuple.Create<int, int>(52, 3490));


运算符

is
运算符:可以检查对象是否与特定的类型兼容。“兼容”表示对象是该类型或者派生自该类型。

as
运算符:用于执行引用类型的显示类型转换。如果要转换的类型与制定的类型兼容,转换就会成功进行;如果类型不兼容,
as
运算符就会返回
null
值。

sizeof
运算符:使用该运算符可以确定栈中值类型需要的长度(单位是字节);如果对于复杂类型(和非基元类型)使用该运算符,就需要把代码写在
unsafe
块中,如:
unsafe{Console.WriteLine(sizeof(Customer));}


可空类型和运算符:通常可空类型与一元或二元运算符一起使用时,如果其中一个操作数或两个操作数都是
null
,其结果就是
null
。如:

int? a = null;

int? b = a + 4; // b = null

int? c = a * 5; // c = null


空合并运算符(??):该运算符提供了一种快捷方式,可以在处理可空类型和引用类型时表示
null
可能的值。这个运算符放在两个操作数之间,第一个操作数必须是一个可空类型或者引用类型;第二个操作数必须与第一个操作数的类型相同,或者可以隐含地转换为第一个操作数的类型。

比较引用类型的相等性

ReferenceEquals()方法:该方法是一个静态方法,测试两个引用是否引用类的同一个实例,特别是两个引用是否包含内存中的相同地址。如果提供的两个引用引用同一个对象实例,则返回
true
,否则返回
false
。但是它认为
null
等于
null
。另外,该方法在应用于值类型时,它总是返回false,因为为了调用这个方法,值类型需要装箱到对象中。

虚拟的Equals()方法:Equals()虚拟版本的
System.Object
实现代码也可以比较引用。但因为这个方法是虚拟的,所以可以在自己的类中重写它,从而按值来比较对象。特别是如果希望类的实例用作字典中的键,就需要重写这个方法,以比较相关值。否则,根据重写
Object.GetHashCode()
的方式,包含对象的字典类要么不工作,要么工作的效率非常低。在重写Equals()方法时要注意,重写的代码不会抛出异常。同理,这是因为如果抛出异常,字典类就会出问题,一些在内部调用这个方法的
.NET
基类也可能出问题。

静态的Equals()方法:Equals()的静态版本与其虚拟实例版本的作用相同,其区别是静态版本带有两个参数,并对它们进行相等比较。这个方法可以处理两个对象中有一个是
null
的情况,因此,如果一个对象可能是
null
,这个方法就可以抛出异常,提供额外保护。静态重载版本首先要检查它传递的引用是否为
null
。如果他们都是
null
,就返回
true
(因为
null
null
相等)。如果只有一个引用是
null
,就返回
false
。如果两个引用实际上引用了某个对象,它就调用Equals()的虚拟实例版本。这表示在重写Equals()的实例版本时,其效果相当于也重写了静态版本。

比较运算符(==):最好将比较运算符看作严格的值比较和严格的引用比较之间的中间选项。在大多数情况下,下面的代码表示正在比较引用:
bool b = (x == y);// x, y object references


运算符重载

运算符重载的声明方式与方法相同,但
operator
关键字告诉编译器,它实际上是一个自定义的运算符重载,后面是相关运算符的实际符号,返回类型是在使用这个运算符时获得的类型。

对于二元运算符(它带两个参数),如
+
-
运算符,第一个参数是运算符左边的值,第二个参数是运算符右边的值。

一般把运算符左边的参数命名为
lhs
,运算符右边的参数命名为
rhs


C#要求所有的运算符重载都声明为
public
static
,这表示它们与它们的类或结构相关联,而不是与某个特定实例相关联,所以运算符重载的代码体不能访问非静态类成员,也不能访问
this
标识符。

C#语言要求成对重载比较运算符。即,如果重载了
==
,也就必须重载
!=
;否则会产生编译错误。另外,比较运算符必须返回布尔类型的值。这是它们与算术运算符的根本区别。

在重载
==
!=
时,还必须重载从
System.Object
中继承的Equals()和GetHashCode()方法,否则会产生一个编译警告。原因是Equals()方法应实现与
==
运算符相同类型的相等逻辑。

委托

理解委托的一个要点是它们的类型安全性非常高。

理解委托的一种好方式是把委托当作这样一件事,它给方法的签名和返回类型指定名称。

Action


Action
是无返回值的泛型委托。

Action
表示无参,无返回值的委托

Action<int,string>
表示有传入参数
int
,
string
无返回值的委托

Action<int,string,bool>
表示有传入参数
int
,
string
,
bool
无返回值的委托

Action<int,int,int,int>
表示有传入4个
int
型参数,无返回值的委托

Action
至少0个参数,至多16个参数,无返回值。

Func


Func
是有返回值的泛型委托

Func<int>
表示无参,返回值为int的委托

Func<object,string,int>
表示传入参数为
object
,
string
返回值为int的委托

Func<object,string,int>
表示传入参数为
object
,
string
返回值为int的委托

Func<T1,T2,,T3,int>
表示传入参数为
T1
,
T2
,
T3
(泛型)返回值为
int
的委托

Func
至少0个参数,至多16个参数,根据返回值泛型返回。必须有返回值,不可
void


Lambda
表达式

只要有委托参数类型的地方,就可以使用
Lambda
表达式。或者说
Lambda
表达式可以用于类型是一个委托的任意地方。

如果只有一个参数,只写出参数名就足够了。如果委托使用多个参数,就把参数名放在小括号中。为了方便可以在小括号中给变量添加参数类型。

如果
Lambda
表达式只有一条语句,在方法块内就不需要花括号和
return
语句,因为编译器会添加一条隐式
return
语句。

正则表达式

常用的特定字符和转义序列如下表:

符 号含 义示 例匹配的示例
^输入文本的开头^BB,但只能是文本中的第一个字符
$输入文本的结尾X$X,但只能是文本中的最后一个字符
.除了换行符(\n)以外的所有单个字符i.ationisation、ization
*可以重复0次或多次的前导字符ra*trt、rat、raat和raaat等
+可以重复1次或多次的前导字符ra+trat、raat和raaat等(但不能是rt)
?可以重复0次或1次的前导字符ra?t只有rt和rat匹配
\s任何空白字符\sa[space]a、\ta、\na(其中[space]表示空格,\t和\n都是转移字符)
\S任何不是空白的字符\SFaF、rF、cF,但不能是\tF
\b字边界ion\b以ion结尾的任何字
\B不是字边界的任意位置\BX\B字中间的任何X
可以把替换的字符放在方括号中,请求匹配包含这些字符。例如,
[1|c]
表示字符可以是
1
c
。在方括号中,也可以指定一个范围,例如
[a-z]
表示所有的小写字母,
[A-E]
表示
A
~
E
之间的所有大写字母(包括字母
A
E
),
[0-9]
表示一个数字。如果要搜索一个整数,就可以编写
[0-9]+


集合

链表。
LinkedList<T>
是一个双向链表,其元素指向它前面和后面的元素。其特点是:插入快,查找慢。

有序列表。如果需要基于键对所需集合排序,就可以使用
SortedList<TKey,TValue>
类,这个类按照键给元素排序。

字典。

字典的主要特征是能根据键快速查找值。也可以自由添加和删除元素,这点有点像
List<T>
类,但没有在内存中移动后续元素的性能开销。

用作字典中键的类型必须重写Object类的GetHashCode()方法。只要字典类需要确定元素的位置,它就要调用GetHashCode()方法。GetHashCode()方法返回的
int
由字典用于计算在对应位置放置元素的索引。

字典的性能取决于GetHashCode()方法的实现代码。

除了实现GetHashCode()方法之外,键类型还必须实现
IEquatable<T>.Equals()
方法,或重写
Object
类的Equals()方法。因为不同的键对象可能返回相同的散列代码,所以字典使用Equals()方法来比较键。

GetHashCode()方法的实现代码必须满足如下要求:

相同的对象应总是返回相同的值。

不同的对象可以返回相同的值。

它应执行得比较快,计算的开销不大。

它不能抛出异常。

它应至少使用一个实例字段。

散列代码值应平均分布在
int
可以存储的整个数字范围上。

散列代码最好在对象的生存期中不发生变化。

如果为Equals()方法提供了重写版本,但没有提供GetHashCode()方法的重写版本,C#编译器就会显示一个编译警告。

LINQ


查询表达式必须以
from
子句开头,以
select
group
子句结束。在这两个子句之间,可以使用
where
orderby
join
let
和其他
from
子句。

LINQ
IEnumerable<T>
接口提供了各种扩展方法,以便用户在实现了该接口的任意集合上使用
LINQ
查询。

释放非托管的资源

在定义一个类时,可以使用两种机制来自动释放非托管的资源。这些机制常常放在一起实现,因为每种机制都为问题提供了略微不同的解决办法。

释放非托管资源的两种机制:声明一个析构函数(或终结器);在类中实现
System.IDisposable
接口。

析构函数

在销毁C++对象时,其析构函数会立即运行。但由于使用C#时垃圾回收器的工作方式,无法确定C#对象的析构函数合适执行。所以,不能在析构函数中放置需要在某一时刻运行的代码,也不应使用能以任意顺序对不同类的实例调用的析构函数。如果对象占用了宝贵而重要的资源,应尽快释放这些资源,此时就不能等待垃圾回收器来释放了。

C#析构函数的实现会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾回收器的一次处理中从内存中删除,但有析构函数的对象需要两次处理才能销毁:第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。另外,运行库使用一个线程来执行所有对象的Finalize()方法。如果频繁使用析构函数,而且使用它们执行长时间的清理任务,对性能的影响就会非常显著。

IDisposable接口

在C#中,推荐使用
System.IDisposable
接口替代析构函数。
IDisposable
接口定义了一种模式(具有语言级的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。
IDisposable
接口声明了一个Dispos()方法,它不带参数,返回
void


Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现
IDisposable
接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。

以上是《C#高级编程》前二十章的读书笔记。笔记摘录了笔者认为易忘的知识点,方便以后查阅和复习。摘抄整理过程中难免疏忽和遗漏,如有错误不当之处,请不吝指出,在此感激不尽!

声明:本文欢迎转载和分享,但是请尊重作者的劳动成果,转载分享时请注明出处:http://blog.csdn.net/wenxin2011/article/details/50790837
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  读书笔记 c#