C#高级编程 读书笔记


.NET Framework 是微软开发的软件环境,而C#是专门用于.NET Framework 的编程语言。
.NET 的公共语言运行库CLR可以将源代码编译成MS的中间语言代码IL,类似于Java字节码:平台无关性(并不完全)、提高性能、语言的互操作性。

.NET运行库对于垃圾回收采用的是垃圾回收器,所有动态请求的内存都分配到堆上,每隔一段时间,当.NET检测到需要清理内存时就调用垃圾回收器。垃圾回收的重要性质是不确定性,也就是说不能保证什么时候会调用垃圾回收器,这个时间由CLR决定。

.NET拥有异常机制来处理错误,也就是try{}catch{}finall{}代码块。

.NET支持特性的使用。

C# 常量(更容易读懂、修改,且更好避免出错)
1.常量必须在声明时初始化且不能用变量来初始化,初始化后不能改写
2.常量用const修饰,不允许再常量声明再包含修饰符static

C# 基本类型
1.值类型:直接存储值,存储再栈上
2.引用类型:存储的是对值得引用地址,通过该地址可以找到值,存储在堆上

Vector x,y;
x = new Vecot();
x.val = 30;
y = x;
Console.WriteLine(y.val);
y.val = 50;
Consolo.WriteLine(x.val);

在上面的程序中,Vector是引用类型,x指向了新建对象(new),然后x的val变成了30,y指向了x,因此第一个Console显示的是 30,然后y的val变成了50,所以第二个Console显示的是50。在这里内存只有一个对象就是new那个,而x指向了该对象,在y = x,y也指向了这个对象,因此x和y指向的是同一个对象,对x和y的操作都会直接影响那个对象。这就是引用类型的特点。而值类型的情况是这样的:声明了一个bool类型的变量,并且赋值false,将这个变量赋值给另一个bool变量,这样会使得内存中存在两个bool值,如果其中的一个值修改为true,那内存中有一个是true另一个是false。

在C#中short,int,long类型的长度并不受平台限制,总是16,32,64位带符号整数。float是32位单精度浮点数,double是64位双精度浮点数。decimal是128位高精度十进制数,如果要指定数字为decimal类型的需要在数字后面加上M。C#的char是16位的Unicode字符。C#支持两种预定义的引用类型object(基类)和string(Unicode字符串)

string类型
string是一种引用类型,他的对象仍然分配在堆上,但是与其他引用类型不完全一样
1.当把一个string变量赋值给另一个string变量的时候,内存中仍然是一个对象两个引用,但是修改其中一个变量的字符串的时候,会在堆上再分配新的string对象,这时内存中就有两个对象,各自有一个引用,修改各自变量的值不会互相影响。

if中的表达式必须等于bool值,不像C++那样可以用整数

C#的switch语句中如果激活了一个较前的case子句,后面的case子句就不会被激活,没有break的case将被编译器视为fault。例外的情况时,如果case子句为空,那就可以从这个case跳到下一个case。
C#的break语句可以用于跳出循环语句,然后执行循环语句后面一条语句。
C#的continue语句用于跳出当前循环迭代,开始执行下一次循环迭代。
C#的return语句用于退出类的方法。
十一
枚举
枚举是用户定义的整数类型,在声明时就要指定枚举值

public enum TimeOfDay
{
	Morning = 0,
	Afternoon = 1,
	Evening = 2
}
//调用TimeOfDay.Morning就会得到0。

十二
using语句
1.用于引入命名空间
2.给类和名称空间指定别名
3.类似try{}catch{}结构
十三
C#的预处理器指令
如 #region和#endregion用于给代码块指定名称
十四
C#的变量名不能是C#关键字且必须以字母或者下划线开头
十五
命名约定
1.推荐camel大小写形式:第一个单词首字母小写,后面单词首字母大写
2.类中私有成员字段名使用camel,并在字段前加一个下划线
3.参数使用camel
4.类名、方法名第一个单词首字母大写,其余单词首字母也大写
十六
结构和类的区别
主要区别是值类型和引用类型的区别
十七
在方法传参时,一般来说,引用类型通过引用传递,值类型通过值传递来传递参数。值传递时,方法获得的是该值的一个数据副本,在方法中对该数据副本作任何操作不会影响原数据;引用传递时,方法获得是这个引用,通过这个引用修改数据会修改原数据。
十八
非要将值类型数据使用引用传递的方式传给方法,比如在该值所占内存很大的时候,可以使用关键字ref,值传递变成引用传递可以避免复制该值减少内存消耗。使用方法是在函数定义的时候,在该参数的类型前面加上关键字ref,同时,在调用函数时,参数变量的前面也加上关键字ref。
另外,C#要求传递给任何方法的任何变量要初始化。
十九
out参数
有时候引用传递传进来的参数的初值没有意义,因为他可能被重写。
但C#又有参数初始化的要求,这时可以使用out关键字,这样做可以使该参数不用被初始化,并且该参数是通过引用传递的。使用方法是在定义函数的时候,在该参数的类型前面加上关键字out,同时,在调用函数的时候在参数变量前面也加上out。
二十
可选参数
在函数传参过程中,有时候有些参数不一定存在,因此可以定义为可选参数。可选参数的位置必须在非可选参数后面并且必须提供默认值。
二十一
方法重载
方法重载是在函数设计的时候函数可能有不同几个版本,实现方法是声明同名但是参数个数或者类型不同的方法。
二十一
一般情况下,C#不需要自己编写类的构造函数,因为编译器在编译时自己会创建默认构造函数。如果需要带参数的构造函数这时候就需要编写构造函数。
二十二
静态构造函数
静态构造函数的使用原因是,类的一些静态字段需要在第一次使用类之前,从外部数据源初始化这些静态字段。静态构造函数不允许private和public这样的访问修饰符修饰。静态构造函数与实例构造函数不同,即使是同参,两个构造函数也可以存在于同一个类定义中,因为静态构造函数在加载类的时候执行,实例构造函数在创建实例时执行。但是由于静态构造函数是在加载类的时候执行,因此当存在多个类都有静态构造函数,就不能确定到底哪个类的静态构造函数先行执行,因此编码不应该有静态构造函数相互依赖的情况。
二十三
只读字段readonly
只读字段readonly与const一样修饰常量,常量是值不能改变的变量。但是readonly更加灵活。readonly允许将字段设置为常量,这个常量来自于某些计算结果。
二十四
匿名类型var
匿名类型只是一个继承自Object且没有名称的类。这个类的定义从初始化器中推断。
二十五
部分类
多个人员开发的时候,类的定义可能会存在于多个文件中,这时候使用部分类可以使得在类定义分布在多个文件中。使用方法是在class前面加上关键字partial
二十六
静态类
静态类只能包含静态属性和静态方法,并不能创建静态类的实例。静态的意思是在程序加载的时候运行,而且只会运行一次。
需要详细介绍静态
二十七
Object类
Object类是.NET的所有类的基类。
二十八
实现继承和接口继承
在C#中,实现继承是一个类派生自一个基类,那子类就拥有基类的成员字段和函数。接口继承是类只是继承了函数的签名,没有继承任何实现代码。
二十九
多重继承
C#不支持多重继承,但是C#允许类继承自多个接口。也就是多重接口继承。在定义类的时候只要把多个接口基类用“ , ”号隔开,就可以实现多重接口继承。
三十
虚方法
把一个基类的函数声明为virtual,就可以在子类中重写该函数。重写的关键字是override。

class MyBaseClass
{
	public virtual string getString()
	{
		retrurn " This is a string ";
	}
}
class MyDerivedClass:MyBaseClass
{
	public override string getString()
	{
		return " This is still a string ";
	}
}

三十一
C#支持子类调用方法的基类版本,只需要在方法名前使用base关键字再 " . "出来。

class MyBaseClass
{
	public virtual decimal GetPrice()
	{
		return 0.00M;
	}
}
public MyDerivedClass:MyBaseClass
{
	public override decimal GerPrice()
	{
		return base.GetPrice()+0.9M;
	}
}

三十二
抽象类和抽象函数
C#允许把类和函数声明为abstract,这就声明了抽象类和抽象函数。抽象类不能实例化。抽象函数不能直接实现,必须在非抽象的派生类中重写。抽象函数本身也是虚拟的,但是已经有abstract关键字了不能再加上virtual关键字。只要类中声明了抽象函数,那类也必须声明为抽象的。
三十三
密封类和密封方法
C#允许将类和方法声明为sealed。对于类,这表示其他类不能继承该类;对于方法,这表示不能重写该方法。如果将方法声明为sealed,则必须在基类上把它声明为virtual或者abstract。
三十四
有继承关系的类的构造函数的执行顺序
当C#实例化一个类对象时,首先会执行它的构造函数。如果类A继承于类B,那实例化类A的时候,类A的构造函数会运行,类A继承于类B,那类A的构造函数会先运行类B的构造函数,类B的构造函数运行的时候,由于类B继承于System.Object,那类B会先运行System.Object的构造函数。这种构造函数的执行顺序总是从基类向下迭代。
三十五
含有基类的类的构造函数调用
一般情况下构造函数是这样定义的

public MyClass()
{

}

实际上构造函数默认按这个样子编译

public MyClass()
:base()//这一行代表在调用这个构造函数的时候,先调用基类的默认构造函数
{

}

按照规则,假如自定义了类的构造函数,那么编译器编译时就不会给类生成默认构造函数
假如有以下代码

public abstract class MyBaseClass()
{
	private string name;
	public MyBaseClass(string name)
	{
	}
}
public class MyGenericClass:MyBaseClass
{
	private string name;
	public MyGenericClass(string name)
	{
	}
}

上面代码会报错,因为在子类中,没有明写出来,调用的就是无参数的基类的构造函数,但是基类定义了有参的 构造函数,没有无参的构造函数,导致编译出错。
子类要调用基类的有参的构造函数方法如下:

public class MyGenericClass:MyBaseClass
{
	private string name;
	public MyGenericClass(string name)
	:base(name)
	{
	}
}

三十六
可见性修饰符
public:所有代码均可访问
protected:只有派生的类型能够访问
private:只能在所属类型中访问
internal:只能在包含它的程序集中访问
protected internal:只能在包含它的程序集和派生类中访问
类可以定义为public或者internal,但是不能定义为其它可见性修饰符,因为没有意义,但可以用来定义类的成员属性和成员方法。
三十七
接口
定义接口不能提供任何成员的实现方式,接口不能有构造函数,也不能被实例化,不能有字段,接口必须实现。接口成员的可见性修饰符总是public,且不用明写出来。
简单说明属性和字段的区别

public class Man()
{
	private int _age;//字段一般私有化,以下划线开头
	public int Age//属性一般公有化,大写字母开头,属性实际上是一个方法
	{
		get { return _age; }
		set { _age = value; }
	}
}

三十八
ArrayList类和List类
ArrayList类存储的是对象,Add()要求把一个对象作为参数。如果ArrayList的Add操作的是一个int类型的整数,这就会发生装箱操作,把一个int类型的值类型的值装箱成一个引用类型的对象。而在读取这个引用的值的时候,需要拆箱操作将对象转换为int类型,这时可以使用类型强制转换运算符。因为ArrayList在这种情况下要进行装箱拆箱,因此性能损失较大。
List类将泛型T定义为int就有List,泛型类不使用对象,而是使用时定义类型。对于List,在Add()一个int值时不需要进行装箱操作,因此在获取值的时候也不需要拆箱。
ArrayList类可以存储不同类型的对象,不是类型安全的;List泛型类在确定泛型T的类型时只能存储类型T的值或者对象,因此是类型安全的。
三十九
当只有一个泛型类的时候,用T作为泛型类的名称;当有多个泛型类,或者泛型类必须实现接口或者派生自基类,泛型类规定用字母T作为前缀,加上描述性名称。
四十
定义以下非泛型的链表类

public classs LinkedListNode
{
	public linkedListNode(object value)
	{
		this.Value = value;
	}
	public object Value { get; private set; }
	public LinkedListNode Next { get; internal set; }
	public LinkedListNode Prev { get; internal set; }
}//class LinkedListNode define END

public class LinkedList:IEnumerable
{
	public LinkedListNode First { get; private set; }
	public LinkedListNode Last { get; private set; }
	public LinkedListNode AddLast(object node)
	{
		var newNode = new LinkedListNode(node);
		if(First == null)
		{
			First = newNode;
			Last = First;
		} 
		else
		{
			LinkedListNode previous = Last;
			Last.Next = newNode;
			Last = newNode;
			Last.Prev = previous ; 
		}
		return newNode;
	}
	public IEnumerator GetEnumerator()
	{
		LinkedListNode current = First;
		while(current != null)
		{
			yield return current.Value;
			current = curren.Next;
		}
	}
}//class LinkedList define END

现在照着上面的代码,写一个链表类的泛型版本

public class LinkedListNode<T>
{
	public LinkedListNode(T value)
	{
		this.Value = value;
	}
	public T Value{ get; private set; }
	public LinkedListNode<T> Next { get; internal set; }
	public LinkedListNode<T> Prev { get; internal set; }
}//class LinkedListNode<T> define END

public class LinkedList<T>:IEnumerable<T>
{
	public LinkedListNode<T> First { get; private set; }
	public LinkedListNode<T> Last { get;private set; }
	public LinkedListNode<T> AddLast(T node)
	{
		var newNode = new LinkedListNode<T>(node);
		if(First == null)
		{	
			First = newNode;
			Last = First;
		}
		else
		{
			LinkedListNode<T> previous = Last;
			Last.Next = newNode;
			Last = newNode;
			Last.Prev = previous;
		}
		return newNode;
	}
	public IEnumerator<T> GetEnumerator()
	{	
		LinkedListNode<T> current = First;
		while(current != null)
		{
			yield return current.Value;
			current = current.Next;
		}
	}
	IEnumerator IEnumerable.GetEnumerator()
	{
		return GetEnumerator();
	}
}//class LinkedList<T> define END

四十一
泛型类初始化的时候为了避免值类型还是引用类型的问题,应该使用default关键字,default关键字可以把值类型初始化为0,引用类型初始化为null。例如

public T GetDocument()
{
	T doc = default(T);
	lock(this)
	{
		doc = docDocument.Dequeue();
	}
	return doc;
}

四十二
泛型约束

public interface TDocument
{
	string Title{ get; set; }
	string Content { get; set; }
}
public class Document:IDocument
{
	public Document()
	{
	}
	public Document(string title,string content)
	{
		this.Title = title;
		this.Content = content;
	}
	public string Title { get; set; }
	public string Content { get; set; }
}

suppose codes in DocumentMananger.cs follows that

public void DisplayAllDocument()
{
	foreach(T doc in docmentQueue)
	{
		Console.WriteLine(((IDocument)doc).Title);
	}
}

由于T是泛型,并不确定是哪个类型,更不能确定这个类型是否实现了IDocument接口,因此假如正好它没有实现,就会导致一个运行时异常,说这个类型强制转换出错。为此最好在定义DocumentMananger时要求泛型类必须实现IDocument接口,这里用TDocument替代T。
codes in DocumentMananger.cs should like this

public class DocumentManager<TDocument>
	where TDocument:IDocument//这个约束说明TDocument要实现IDocument接口
{

}

泛型中有以下约束,
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#里面子类对象是可以赋值给基类引用的,因为子类对象符合基类的任何特点。
四十四
泛型方法

//define function
public void Swap<T>(ref T x,ref T y)
{
	T temp;
	temp = x;
	x = y;
	y = temp;
}
//use function
Swap<int>(ref 4,ref 5);
Swap(ref 4, ref5);//泛型方法可以像非泛型方法那样调用

四十五
泛型方法可以重载,这就关系到调用重载方法的参数匹配问题。调用重载方法的参数匹配的时机是在编译期间,编译器会选择最佳参数类型匹配。

public class MethodOverloads
{
	public void Foo(int x)
	{
		Console.WriteLine(" Foo(int x) ");
	}
	public void Foo<T>(T obj)
	{
		Conosle.WriteLine(" Foo<T>(T obj)  and type: {0} ",obj.GetType().Name);
	}
	public void Bar<T>(T obj)
	{
		Foo(obj);
	}
}
//codes in Main Function
var test = new MethodOverloads();
test.Foo(11);
test.Foo("ab");
test.Bar(44);
//console writes
Foo(int x)
Foo<T>(T obj) and type: String
Foo<T>(T obj) and type:  Int32

第一个输出毫无悬念;第二个输出基于最佳匹配,没有string类型参数的Foo函数,自然匹配泛型参数的Foo函数;第三个输出,重载函数的参数匹配是在编译期间的,所以泛型参数的Bar函数会调用泛型参数的Foo函数(最佳匹配,大家参数类型都是泛型),而函数传参的时机是在运行期间,所以传递的参数类型是Int类型。
四十六
数组是引用类型,在用new初始化的时候可以给出数组大小,如果不知道数组大小,请使用集合。数组大小一旦指定以后不能改变。用这种形式定义数组可以不指定数组大小,但是编译器会统计得出数组大小。假如数组中的元素是引用类型,那数组中的每个元素必须要分配内存,否则抛出异常。

int []myArr = new int[]{ 4, 7, 8 , 10 };
//等价于int []myArr = new int[4]{ 4, 7, 8 , 10 };
//等价于int []myArr = { 4, 7, 8 , 10 };

内存分配情况
引用类型的对象存储在堆,那对象的各种属性值肯定也在堆啊。
在这里插入图片描述二维数组的定义

int[,] twodim = new int[3,3];

锯齿数组的定义

int[][] jagged = new int[3][];
jagged[0] = new int[2]{1,2};
jagged[1] = new int[6]{3,4,5,6,7,8};
jagged[2] = new int[3]{9,10,11};

四十七
Array类
Array类是一个抽象类,可以用CreateInstance()方法创建数组,用SetValue()方法设置值,用GetVlaue()方法获取值。
四十八
复制数组
C#的数组有Clone()方法可以用来复制数组,如果数组元素是值类型,则复制值;如果数组元素是引用类型,则复制引用,这就有复制的引用指向相同的对象。
四十八
三元运算符
?号左边是一个bool表达式,假如为true,则整个式子的返回值是"boy",假如为false,则整个式子的返回值是"girl"。

a==b?"boy":"girl"

四十九
is运算符
is运算符可以用来检查对象(值)是否与指定类型兼容。

if(“JACK” is string)
{
	Console.Write(“ TRUE ”);
}

五十
as运算符
as运算符用来执行引用类型的显式类型转换,如果兼容,则转换成功;如果不兼容,则转换就会返回null。
五十一
sizeof运算符
sizeof运算符用来确定值类型的长度,以字节为单位。
五十二
可空类型
在类型后面加上 ? 号就代表该类型为可空类型,就是可以为null值。在含有可空类型的一元或者二元运算中,只要有一个操作数为null结果就是null;而在比较可空类型时,只要有一个操作数为null,结果就是false。
五十三
空合并运算符??
空合并运算符在两个操作数之间,第一个操作数必须是可空类型或者引用类型,第二个操作数必须与第一个操作数类型兼容。如果第一个操作数不是null,整个表达式等于第一个操作数的值;如果第一个操作数是null,则整个表达式等于第二个操作数的值。

int? a = null;
int b;
b = a ?? 10;//b has the value 10
a = 3;
b= a ?? 10;//b has the value 3

五十四
整数类型的类型隐式类型只能从字节数较小的类型转换为字节数较大的类型;整数和浮点数之间也存在类型隐式转换,只要无符号变量在有符号变量的范围内,那无符号变量也可以隐式转换成有符号变量。
可空类型可以隐式转换成其他可空类型,但要遵循规则,同样,非可空类型也可以隐式转换成可控类型,同样要遵守规则。可空类型不能隐式转换成非可空类型。
强制类型转换(类型显式转换cast)要小心使用,可能会引发异常。
五十五
装箱,用于把一个值类型数据转换为引用类型,这个转换可以显示也可以隐式。
拆箱用于把以前装箱的值类型强制转换为值类型,这个转换只能是显示的。
五十六
有四种比较引用类型相等性的方法。

  1. ReferenceEquals()
    是一个静态方法,所以不能重写。比较的是两个引用是否指向同一块内存地址。
  2. 虚拟的Equals()
    比较的是引用,可以重写。
  3. 静态的Equals()
    高级版本的虚拟的Equals()方法,它用来比较对象,可以比较有null的情况。假如两个参数都是null,则返回true;假如其中一个为null,则返回false;假如都不是null,则调用虚拟的Equals()。
  4. 比较运算符 ==
    当比较引用类型时,是比较他们的引用,除了System.String类外,因为它的对象比较的是值(Microsoft重写了这个运算符)。
    比较值类型可以用Equals()和比较运算符 ==来比较值。
    五十七
    运算符重载
    C#要求运算符重载要成对重载比如>=和<=,==和!=
    五十八
    委托是类型安全的类,它定义了返回类型和参数的类型。
    委托的声明
delegate void IntMethodInvoker(int x);//说明作为参数的方法返回类型为void,有一个int类型的参数。

委托的调用

private delegate string GetString();
static void Main()
{
	int x = 40;
	GrtString myString =new GetString(x.Tostring);
	Console.WriteLine("String is {0}" , myString());
}

委托示例

//MathOperations.cs
class MathOperations
{
	public static double MultiplyByTwo(double value)	
	{	
		return value*2;
	}
	public  static double Square(double value)
	{	
		return value*value;
	}
}
//Program.cs
using System;
namespace Workx.ProCSharp.Delegates
{
	delegate double DoubleOp(double x);
	class Program
	{	
		DoubleOp[] operations=
		{
			MathOperations.MultiplyByTwo,
			MathOperations.Square
		};
		for(int i = 0;i < operations.Length ; i++)
		{
			Console.WriteLine("Using operations[{0}] : ",i);
			ProcessAndDisPlayNumber(operations[i],2.0);
			ProcessAndDisPlayNumber(operations[i],7.4);
			ProcessAndDisPlayNumber(operations[i],1.414);
			Console.WriteLine();
		}
	}
	static void ProcessAndDisplayNumber(DoubleOp action,double value)
	{	
		double result = action(value);
		Console.WriteLine("Value is {0},result of operation is {1}",value,result);
	}
}

五十九
重要泛型委托Action和Func
泛型委托Action表示引用一个void返回类型的方法。这个委托可以引用带有至少1个至多16个参数类型的方法。
Action表示引用带有一个泛型参数的方法,Action<in T1,in T2>表示引用带有两个泛型参数的方法,以此类推。
泛型委托Func允许引用带返回类型的方法。可以引用带有至少0个至多16个参数类型的方法。
Func表示引用带有0个参数和具有返回类型的方法,Func<in T,out TResult>表示引用带有一个泛型参数和具有返回类型的方法。

//MathOperations类如【五十八】定义
Func<double,double>[] operations = 
{
	MathOperations.MultiplyByTwo,
	MathOperations.Square
};
//则ProcessAndDisplayNumber()如下
static void ProcessAndDisplayNumber(Func<double,double> action,double value)
{	
	double result = action(value);
	Console.WriteLine("Value is {0},result of operation is {1}",value,result);
}

六十
多播委托
可以用委托数组来包含对多个方法的引用,也可以使用多播委托来实现。调用多播委托,可以顺序连续调用多个方法,但是多播委托必须是以void为返回类型的。多播委托支持 + 和+=来添加引用,也支持 - 和 -= 去除引用。

Action<double> operations = MathOperations.MultiplyByTwo;
operations += MathOperations.Square;

使用多播委托时可以用GetInvocationList()方法返回数组类型Delegate[]对象。
六十一
lambda表达式的运算符 => 读作 “goes to”,它的左边定义了需要的参数,右边则是使用了左边参数的方法实现。

//EXP 1
Func<string,string> oneParam = s=>String.Format("change uppercase {0}",s.ToUpper());
//EXP 2
Func<double,double,double> twoParam=(x,y)=>x * y;
//EXP 3
Func<string,string> lambda = param=>
{
	param+=1;
	return param;
}

六十二
通过lambda表达式可以访问lambda表达式块外部的变量,这称为闭包。
六十三
事件基于委托,例如Button的Click事件,这类事件就是委托。

猜你喜欢

转载自blog.csdn.net/qq_31729917/article/details/83829294