码迷,mamicode.com
首页 > Windows程序 > 详细

[WuDe]C#程序设计教程 - 第2章 C#面向对象基础

时间:2014-12-10 12:25:59      阅读:350      评论:0      收藏:0      [点我收藏+]

标签:style   blog   http   io   ar   color   os   使用   sp   

bubuko.com,布布扣

第2章 C#面向对象基础

2.1 类

类是一种数据类型,而对象是具有这种类型的变量

[类的修饰符] class 类名 [:基类名]

{

//类的成员

}[;]

访问级别的用处在于控制成员在哪些地方可以被访问,这样达到面向对象中"封装”的目的;控制对外访问权限

在类这个级别,不写访问修饰符默认为internal,类只有两个访问修饰符,public 和internal (暂时这样理解),在类里面,方法外定义变量或方法前不加访问修饰符,默认为private

Public公共类:不受限制对该类的访问

Protected保护类:当前类和子类可以访问

Internal内部类:当前程序集可以访问

ProtectedInternal:同一个程序集中的本类及子类中可以访问

Private私有类:只有当前类可以访问

Abstract抽象类:表示该类是一个不完整的类,不允许建立类的实例

Sealed密封类:不允许从该类派生新的类

具有相同属性和行为的对象的抽象的集合

定义在类里面的都叫做类的成员

类的成员

类的成员可以分为两大类:类本身所声明的以及从基类中继承而来的;

类的成员

字段:字段存储类要满足其设计所需要的数据,亦称为数据成员

属性:属性是类中可以像类中的字段一样被访问的方法;属性封装字段;

方法:方法定义类可以执行的操作;

事件:事件是向其他对象提供有关事件发生通知的一种方式

索引器:索引器允许以类似于数组的方式为对象建立索引

运算符:运算符是对操作数执行运算的术语或符号,如 + * < 等

构造函数:构造函数是在第一次创建对象时调用的方法,它们通常用于初始化数据

析构函数:析构函数是当对象即将从内存中移除时由运行库执行引擎调用的方法.它们通常用来确保需要释放的所有资源都得到了适当的处理;

类的修饰符

Public 公共成员:

Private 私有成员:

Protected 保护成员:

Internal 内部成员:

Readonly 只读成员:这类成员的值只能读,不能写;也就是说,出了赋予初始值外,在程序的任何部分将无法更改这个成员的值;

分部类

分部类 partial 又称伙伴类,被partial关键字修饰的类,如果这几个类类名一致则编译器会将这些部分类会编译成一个类;

分部类可以将类(结构或接口等)的声明拆分到两个或多个源文件中;分部类的每一个部分都可以存放在不同的文件中,编译时会自动将所有的部分组合起来构成一个完整的类声明;

静态与非静态之间的调用方式:

bubuko.com,布布扣 在单个类中:静态不能直接调用非静态成员(属性,方法),只能通过实例化的对象调用

bubuko.com,布布扣 在单个类中:静态可以直接调用静态成员

bubuko.com,布布扣 在单个类中:实例成员可以自由调用静态成员(银行 vip---非vip)

bubuko.com,布布扣 在不同类中:实例成员通过类的对象调用,而静态成员通过类名调用

bubuko.com,布布扣 为什么静态不能直接调用非静态成员:静态成员与类一起加载,在加载静态成员的时候,非静态成员还没有生成

类与对象的关系:

类是用来描述事物的,是模型,是一个概念,它的成员:属性(描述的对象的特征) 方法(描述的对象的行为)

对象是类的实例,是具体的存在。

2.2 命名空间

命名空间可以重名,如果重名表示这些命名下面的类都是属于同一个命名空间用于解决类重名问题,可以看做类的”文件夹”;

注:当想调用的类处于不同的命名空间时,可以通过两种方法访问:

一是通过全名称.类名来访问

二是通过using添加该类所在的命名空间

在同一个命名空间下不能定义两个相同名字的类;

程序集类似与“硬盘”,那么程序集里的命名空间就是硬盘的“文件夹”,类就是文件夹的文件

2.3 面向对象其实就是思考问题的一种方式

面向对象不是替代面向过程的,是基于面向过程的;由执行者变为指挥者
好处:便于维护和修改

2.4 对象

具体存在的个体;看得见摸得着,真实存在的

类和对象是不同的概念,类定义对象的类型,对象是基于类的具体实体,有时称为类的实例。只有定义类对象时才会给对象分配相应的内存空间。

定义类的对象

一旦声明了一个类,就可以用它作为数据类型来定义类对象(简称对象),定义类的对象分为2步

定义对象的引用

类名 对象名;

例如: Person p;

创建类的实例

对象名 = new 类名();

p = new Perseon();

以上可以写为: Person p = new Person();

new关键字做的事情:

1) 在堆空间里开辟一块合适的空间(合适是根据类的字段大小来去确定的)

2) 创建对象

3) 调用构造函数

4) 返回堆空间中对象的引用地址

类里面的对象都是存放在堆空间内的,不管是引用类型还是值类型;对应的栈空间的变量保存的是堆空间内存放对象的地址

只将类中的字段及属性存放在特定的空间内,类中的方法可以存放在方法表里面

访问类里面的方法通过对象名.方法名来调用

数据类型就是存储数据的模板 微软实现帮我们定义了N中模板

program是微软定义的默认类

this关键字代表当前对象可以显式调用对象,增强代码的阅读性

访问对象的字段

对象名.字段名

其中”.”是一个运算符,该运算符的功能是表示对象的成员

调用对象的方法

对象名.方法名(参数表)

2.5 构造函数和析构函数

构造函数

1) 构造函数是在在创建给定类型的对象时执行的类的方法;

2) 构造函数名称与类名相同

3) 构造函数是一种特殊的方法,可以构成重载

4) 当类对象创建时,构造函数会自动执行;由于它们没有返回类型,因此不能像其他函数那样进行调用

5) 当类对象声明时,调用哪一个构造函数取决于,传递给他的参数类型

6) 构造函数不能被继承

7) 构造函数尽管是一个函数,但没有任何类型,即它既不属于返回值函数也不属于void函数

它就是构建类的对象用的。一个特殊的方法 返回值,重载,传递参数

没有返回值,名称和类名一致,任何成员(除了构造函数)名称都不能和类名一致

每一个类都会有一个默认的无参的构造函数。

构造函数不能像普通方法一样显示调用,只能创建对象的时候调用,以及子类显示的调用父类的构造函数

如果人为添加了带参的构造函数,那么系统默认生成的无参构造函数会自动消失。所以一般我们应该养成一个习惯:创建好一个类,立刻为其添加无参的构造函数。

构造函数也可以重载。 方法名称一致, 参数不一致(类型不一致,个数不一致)

隐式构造函数

如果我们没有为类自定义任何的构造函数,那么C#编译器会自动的为这个类添加一个没有参数的没有方法体的构造函数;一旦我们为这个类添加了自定义的构造函数,那么,C#编译器就不再为这个类添加无参无方法体的构造函数;如果需要访问无参的构造函数,可以自己写一个无参的

调用构造函数

在构造函数里不能直接的去调用另外一个构造函数;但是可以直接调用成员方法;在成员方法中不可以调用构造函数

通过":this"(public Person(string name,int age):this(这里是想调用的参数类型))可以调用本类的其他构造函数,调用其他的构造函数时也可以重载调用,如果在构造函数中调用其他的构造函数会当要进入这个构造函数之前先执行被调用的构造函数

构造函数的作用:对对象进行初始化,只要你想要代码被执行都可以把代码写进构造函数中,不过一般情况下我们是对对象进行赋初始值

析构函数

在对象不再需要时,希望确保函数所占的存储空间能被回收.C#提供了析构函数用于专门释放被占用的系统资源;

析构函数在类对象销毁时自动执行

一个类只能有一个析构函数,而且析构函数没有参数

析构函数的名称是”~”加上类名(中间没有空格)

与构造函数一样,析构函数也没有返回类型

析构函数不能被继承

析构函数没有访问修饰符

不能被调用,只能被GC

当一个对象被系统销毁时自动调用类的析构函数

~Person(){ …}

2.6 静态成员

静态成员包括静态字段和静态方法.静态成员属于类所有,而非静态成员属于类的对象所有.提出静态成员的目的是为了解决数据共享问题

静态字段是类中所有对象共享的成员,而不是某个对象的成员,也就是静态字段的存储空间不是放在每个对象中,而是和方法一样放在类的公共区中

静态字段的定义需要在前面加上static关键字;

静态字段的访问:类名.静态字段名

静态方法与静态字段类似,也是从属于类,都是类的静态成员.只要类存在,静态方法就可以使用,静态方法的定义是加上static关键字。

调用静态方法:类名.静态方法名( 参数列表 );

注意:静态方法只能访问静态字段/其他静态方法和类以外的函数及数据,不能访问类中的非静态成员(因为非静态成员只有对象存在时才有意义);但静态字段和静态方法可由任意访问权限许可的成员访问;

静态成员

一个类的方法有静态方法和非静态方法之分;对于静态方法,只能通过类名来调用,而对用非静态方法,需通过类的对象来调用;

(1)什么是静态成员,怎样访问?
用static关键字修饰的成员就叫静态成员,访问时应该通过类名.静态成员名来访问
(2)实例成员就是没有static修饰的成员叫做实例成员
(3)静态成员与实例成员的区别:
访问方式不同:
静态成员是属于类的,所以只能通过类名.静态成员来访问
实例成员是属于对象的,可以通过对象名.实例成员来访问
存放位置不同:静态成员存放在静态存储区里面,实例成员是跟着对象存在堆空间内
生命周期不同:静态成员是从类被第一次访问时加载到程序退出时结束,实例成员是在对象被创建到被垃圾回收机制回收时结束
(4)静态成员在内存里的存储时机?
静态成员只被创建一次,存储在静态存储区;这个类第一次被访问的时候
类加载,第一次访问类的时候;会将类中所有的静态成员加载到静态存储区里
(5)静态成员的生命周期? 静态成员一旦被加载直到程序退出才被释放
什么情况下需要定义成为静态成员?
(需要全局访问(需要被共享的成员),节约空间提高效率)
不要随意定义静态成员
(6)在静态方法中不能直接访问实例成员
在实例方法中可以直接访问静态成员 在同一个方法中访问静态成员可以不写类名
定义在方法里面的局部变量出了作用域就会被回收,而静态成员是从被加载到退出程序都是存在的,所以在方法里不能定义静态变量;因为静态成员存在的时候,实例还不存在
(7)静态构造函数:1.什么时候被调用;2.可以传参数吗;3.可以重载吗?
1.第一次访问这个类的成员的时候,静态构造函数会被调用
2.静态的构造函数不能有参数,不能重载,没有访问修饰符
(8)被static修饰的类叫做静态类
1.静态类中只能有静态成员
2.静态类中不能被实例化,因为静态类中只能有静态成员,创建对象没有意义
3.静态类不能被继承
4.什么时候要定义为静态类:这个类中的成员需要经常被调用,被共享
(9)直接定义在类下面的是类的成员,包括字段属性方法,根据字段来开辟空间,代码块只能写在方法里

静态与非静态之间的调用方式:

在单个类中:静态不能直接调用非静态成员(属性,方法),只能通过实例化的对象调用

在单个类中:静态可以直接调用静态成员

在单个类中:实例成员可以自由调用静态成员(银行 vip---非vip)

在不同类中:实例成员通过类的对象调用,而静态成员通过类名调用

为什么静态不能直接调用非静态成员:静态成员与类一起加载,在加载静态成员的时候,非静态成员还没有生成

2.7 属性

属性描述了对象的具体特性,它提供了对类或对象成员的访问.C#中的属性更充分地体现了对象的封装性,属性不直接操作类的字段,而是通过访问器进行访问.

属性的声明

修饰符 数据类型 属性名称

{

Get 访问器

Set 访问器

}

其中,修饰符有new/public/protected/internal/private/static/virtual/override/abstract.

属性是通过访问器实现的.访问器是数据字段赋值和检索其值的特殊方法使用Set访问器可以为数据字段赋值,使用get访问器可以检索数据字段的值;

只有set访问器,表示该属性只写;

只有get放温情,表示该属性只读;

属性是为了保护类的字段.通常情况下将字段设置为私有的,设计一个对其进行读或写的属性.在属性的get访问器中,用return来返回该字段的值,在属性的set访问器中可以使用一个特殊的隐含参数value,该参数包含用户指定的值;

属性是对象具有的各种特征:

属性的本质是1个set方法和1个get方法;

get方法时负责返回封装的字段的值;

set方法就是负责封装的字段赋值,定义的参数是value;

对字段的封装 可以控制对字段的赋值和取值,之前做一些逻辑上的处理

字段命名规则 首字母小写

属性命名规则 首字母大写,如果只写get不写set代表这个属性是只读的
属性的名字不一定要与被封装的字段名字一样,属性首字母大写是为了方便阅读,一眼就看到是对哪个字段进行封装;

C#里面尽量所有的字段都封装为属性;

如果属性没有对字段做任何逻辑上的判断,那么字段可以省略定义;(C#编译器自动为属性生成一个字段来被这个属性封装<字段名>k_BackingField)如果要控制只读只写 简写的方式不可用简写需要(1.没有逻辑上的更改 2.支持只读只写操作)快捷键:Ctrl+R+E自动属性快捷键 prop自动属性:如果对读取操作没有任何逻辑上的判断和操作 可以直接写成自动属性public int Age{get;set;}

Int age;

Public int Age

{

Get

{

Return age;

}

Set

{

age = value;

}

}

set

{

if (value > 0 && value <= 100)

{

age = value;

}

else

{

age = 18;

}

}

2.8 方法

从本质上讲,方法就是和类相关联的动作,是类的外部界面,用户可以通过外部界面来操作类的私有字段;

方法是一种行为、动作,执行的操作

修饰符 返回类型 方法名( 参数列表 )

{

//方法的具体实现
}

public :使用它来定义的成员可以在所有类里面使用(不同项目需要先引用)

private:私有的,只有在当前类中可以使用

protected:使用它来定义的成员只有在当前类或者当前类的子类中使用

internal:在当前整个程序集中可以使用

返回值类型

void:没有返回值.(可以写return(可以提前终止方法),但是不能return具体的值)

非void,必须返回相应类型的值(任何类型都可以做为方法的返回值类型)

默认修饰符是private,如果没有返回值类型就写”void”,如果有返回值类型,则需要用return关键字来返回值; 这里的参数是形参,本质上是变量,它用来在调用方法时接收实参传给方法的值,如果方法没有参数,那么参数列表为空;

return立即跳出这个方法,后面若有代码将不再执行;

当调用者想访问被调用者方法里面的值时,有两种方法:

1) 一是将想访问的变量定义到方法外面

2) 二是通过返回值

方法的参数

方法中的参数是保证不同方法间互动的重要桥梁,方便用户对数据的操作.C#中方法参数有4种类型;

形参:只是一个参数的声明,说明调用的时候需要传递过来的值(类型一致,个数一致,顺序一致);形参是实参的规范。

? 值参数

不含有任何修饰符,当利用值向方法传递参数时,编译程序给实参的值做一份备份,并且将此备份传递给该方法,被调用的方法不会修改内存中的实参的值,所以使用值参数时可以保证实际参数的安全性.在调用方法时,如果形参的类型是值参数的话,调用的实参的表达式必须保证是正确的值的表达式;

? 引用型参数

以ref修饰符声明的参数属引用型参数.引用型参数本身并不创建新的存储空间,而是将实参的地址传递给形参,所以对形参的修改会影响原来实参的值.在调用方法前,引用型实参必须被初始化,同时在调用方法时,对应引用型参数的实参也必须使用ref修饰;

Void add(int num,ref int x)

{

x = num;

}

Int x = 0; //使用ref传值必须初始化

Add(5,ref x);

? 输出参数

以out修饰符声明的参数属于输出参数.与引用型参数类似,输出型参数也不开辟新的内存区域.

它与引用型参数的差别在于,调用方法前无需对变量进行初始化;输出型参数用于传递方法返回的数据,out修饰符后应跟随与形参的类型相同的类型,用来表明在方法返回后传递的变量经过了初始化。

Void add(int num,out int x)

{

x = num; //输出参数必须为out x 赋值

}

Int x; //使用out传值不需要初始化

Add(5,outx);

? 数组型参数

以params修饰符声明的参数属数组型参数.

Params关键字可以指定在参数数目可变处采用参数的方法参数.

在声明中params关键字之后不允许任何其他参数(多个类型不同的参数时,params要放到最后面),并且在方法声明中只允许一个patams关键字.有数组型参数就不能使用ref和out修饰符(这里表示不能同时修饰一个参数).

params int [] arr

arr为一个可变数组 可以给多个元素也可以只给一个 也可以不给,若一个参数都不给 则arr.length为0 表示用户没有传递参数进来, 若方法中有不同类型的多个参数 可变参数必须放在最后且只能有一个。

如果没有传递任何参数,需要做一个判断,不然可能造成索引越界的错误

protected void Button1_Click(object sender, EventArgs e)
{
//冒泡排序,倒序
int[] arr = new int[6] { 2, 34, -56, 9, 43, 5 };
int temp;
for (int i = 0; i < arr.Length - 1; i++)
{
for (int j = 0; j < arr.Length - 1 - i; j++)
{
if (arr[j + 1] > arr[j])
{
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
} 
}
Response.Write(Test(arr));
}
string Test(params int[] str)
{
//params 用来修饰方法参数的,只能用来修饰一维数组的参数
//可以传入一个数组,也可传入0个或多个相应类型的参数
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < str.Length; i++)
{
sb.Append("这个是:" + str[i] + "<br/>");
}
return sb.ToString();
}

方法的重载

方法的重载是指调用同一个方法名,但是使用不同的数据类型参数或者次序不一致的参数。

与返回值无关;这些方法需要再同一个类中。

方法重载的作用:可以在不改变原方法的基础上新增功能,提供了函数可扩展的能力;

为此,C#引入了成员签名的概念.成员的签名包含成员的名称和参数表,每个成员签名在类型中必须是唯一的,只要成员的参数列表不同,成员的名称可以相同.如果同一个类有两个或多个这样的成员(方法/属性/构造函数等),它们具有相同的名称和不同的参数列表,则称该同类成员进行了重载,但它们的成员签名是不同的.

static void Main(string[] args)
{
int x =0;
int[] arr = new int[] { 1,2,3,45,88,54,5 };
Add(ref x, arr);
Console.WriteLine("{0}",x);
Console.ReadLine();
}
public static void Add(ref int num ,params int[] arr)
{
foreach(int item in arr)
{
num+=item;
}
}

2.9 索引器

2.10 委托

事件与委托详情请参考:

http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html

委托就是能存放符合某种格式(方法签名)的方法的指针的容器;

通过使用Delegate类,委托实例可以封装属于可调用实体的方法;

委托的特点:

委托类似与C++函数指针,但它是类型安全的

委托允许将方法作为参数进行传递

委托可用于定义回调方法

委托可以链接在一起.例如,可以对一个事件调用多个方法

方法不需要与委托签名精确匹配

定义和使用委托

定义委托有3个步骤,即,声明 -> 实例化 -> 调用

声明委托类型

声明委托类型就是告诉编译器这种类型代表了哪种类型方法.

[修饰符] delegate 返回类型委托类型名 ( 参数列表 );

每当需要一组新的参数类型或新的返回类型时,都必须声明一个新的委托类型;例如:private delegate void mydelegate (int b);

上述代码声明一个委托mydelegate,该委托类型可以引用一个采用int作为参数并返回void方法;

实例化委托

声明了委托类型后,必须创建一个它的实例,即创建委托对象并使之与特定方法关联.

定义委托对象的语法格式如下:

委托类型名委托对象名;

例如:mydelegate p;

另外,委托对象还需实例化为调用的方法,通常将这写方法放在一个类中(也可以将这些方法放在程序的Program类中);

例如:

classProgram
{
Private delegate void mydelegate(int n);
staticvoid Main(string[] args)
{
mydelegate p;
p = fun1;
p += fun2;//
p(100);//调用委托
Console.ReadKey();
}
Public static void fun1(int n)
{
Console.WriteLine("{0}的2倍是{1}", n, 2 * n);
}
Public static void fun2(int n)
{
Console.WriteLine("{0}的3倍是{1}", n, 3 * n);
}
}

调用委托

创建委托对象后,通常将委托对象传递给将调用该委托的其他代码.通过委托对象的名称(后面跟着要传递给委托的参数,放在括号内)调用委托对象.

委托对象名(实参列表);

例如:p(100);

委托对象是不可变的,即设置与它们匹配的签名后就不能再更改签名了,但是,如果其他方法具有同一签名,也可以指向该方法;

例如:

p = new mydelegate(fun1); //可以简写为 p = fun1;

p(100);

p = new mydelegate(fun2); //可以简写为p = fun2;

p(200);

对于p(100)的执行过程是,p是一个委托对象(类似与C/C++中的函数指针),它已指向fun1事件处理方法,现在将参数100传递给fun1方法,然后执行该方法,相当于执行fun1(100).和C/C++中函数指针不同的是,一个委托对象可以指向多个事件处理方法(本例只指向一个事件处理方法),从而激活多个事件处理方法的执行;

委托对象封装多个方法

委托对象可以封装多个方法,这些方法的集合称为调用列表.委托使用”+”,”+”,”+=”,”-=”等运算符向调用列表中增加或移除事件处理方法;

P是一个委托对象,它已指向这两个事件处理方法,将参数传递给这2个事件处理方法,并分别执行这些方法;相当于fun1(100);fun2(100);

使用委托与匿名方法关联

所谓匿名方法就是没有方法名称的方法.当将委托与匿名方法关联时,直接给出方法的函数体,其一般格式如下;

Delegate 返回类型委托类型名( 参数列表 );

委托类型名委托对象名= 返回类型( 参数列表 ) { /* 匿名方法代码 */ };

委托对象名( 实参列表 );

第1步,声明委托类型;第2步,定义匿名方法并将其与委托对象关联;第3步,调用委托

例如:

Private delegate void mydelegate(string str);

staticvoid Main(string[] args)

{

//mydelegate p;

//p = fun1;

//p += fun2; //

//p(100); //调用委托

mydelegate p = delegate(string str)

{

Console.WriteLine(str);

};

p("输出...");

Console.ReadKey();

}

2.11 事件

事件是类在发生其关注的事情时用来提供通知的一种方式;

事件的创建和使用

n 为事件创建一个委托类型

所有的事件是通过委托来激活的,其返回值类型一般为void型.为事件创建一个委托类的语法格式如下:

Delegate void 委托类型名([触发事件的对象名.事件参数])

例如:

Public delegate void mydelegate(); //无参委托

n 创建事件处理方法

当事件触发时要调用事件处理方法,需要设计相应的事件处理方法,可以将它放在单独类中,也可以放在触发事件类中;

例如:

n 声明事件

事件是类成员,以关键字event声明

[修饰符] event 委托类型名事件名;

一般在声明事件的类中包含触发事件的方法.例如:以下MyEvent类包含事件声明和触发该事件的方法;

n 通过委托对象来掉用被包含的方法

向类事件(列表)中添加事件处理方法的一个委托,这个过程称为订阅事件该过程通常是在主程序中进行,首先必须声明一个包含事件的类的对象,然后将事件处理方法和该对象关联起来,其格式如下:

事件类对象名.事件名+= new 委托类型名( 事件处理方法 );

+=,-+,+,-等运算符添加或删除事件处理方法.最后调用触发事件的方法便可触发事件.

例如:

Public delegate void mydelegate();

Class Program

{

Static void Main(string[] args)

{

MyEventHandler a = newMyEventHandler();

MyEvent b = newMyEvent();

//b.Event1 += new mydelegate(a.OnHandler1);

b.Event1 += a.OnHandler1;

b.FirstEvent();

Console.ReadKey();

}

}

classMyEvent

{

publiceventmydelegate Event1;

publicvoid FirstEvent()

{

if (Event1 != null)

{

Event1();

}

}

}

classMyEventHandler

{

publicvoid OnHandler1()

{

Console.WriteLine("调用OnHandler1方法");

}

}

2.12 委托和事件(扩展与总结)

http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html

张子阳-C#中的委托和事件

1. 将方法作为方法的参数

传递参数,就是对参数进行赋值,就是将方法赋给委托。

委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。实际上,委托在编译的时候确实会编译成类。因为Delegate是一个类,所以在任何可以声明类的地方都可以声明委托。

2. 将方法绑定到委托

使用委托可以将多个方法绑定到同一个委托变量,当调用此变量时(这里用“调用”这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。

3. Private GreetingDelegate MakeGreet; //对事件的声明 实际是 声明一个私有的委托变量

通过反编译,可以验证;

Event封装了委托类型的变量,使得,在类的内部,不管你声明它是public还是protected,它总是private的。在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。

很容易注意到:MakeGreet 事件的声明与之前委托变量delegate1的声明唯一的区别是多了一个event关键字。看到这里,在结合上面的讲解,你应该明白到:事件其实没什么不好理解的,声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。

4. ‘

5. ‘

6. ‘

7.

2.13 运算符重载(P119)

运算符重载概述

一元运算符重载

二元运算符重载

2.14 类的转换

关键字 is

Is是一个检查引用类型变量的运算符;

如果所提供的表达式非空,并且所提供的对象可以强制转换为所提供的类型而不会导致引发异常,则is表达式的结果将是true;

如果一直表达式始终是true或false,则is关键字将导致编译时警告,但是,通常在运行时才计算类型兼容性;

Is运算符不能重载;

注意 : Is运算符只考虑引用转换/装箱转换和取消装箱转换,不考虑其他转换,如用户的自定义转换;

is是用来判断指定的对象是否是指定的类型;is可以判断引用类型,也能判断值类型(仅限于同种类型)

关键字as

As 用于在兼容的引用类型之间执行转换.

As 运算符类似与强制转换,所不同的是,当转换失败时,运算符将产生空(null),而不是引发异常。

表达式 as 数据类型

等效于

表达式 is 数据类型? (数据类型)表达式 : (数据类型)null

其中上述表达式只被计算一次.

注意 : as 运算符只执行引用转换和装箱转换.as运算符无法执行其他转换,如用户的自定义转换,这类转换应使用cast表达式执行;

例如:以下执行装箱转换

Int I= 1;

Object obj = I as object;

若提示: xxxxxx未标记为可序列化; 在类/方法/属性前添加 [Serializable] 特性即可

类型转换int.Parse()与int.TryParse()

int Parse(string s);

转换不成功,则报异常

摘要:将数字的字符串表示形式转换为它的等效 32 位有符号整数

异常:

System.ArgumentNullException:

s 为 null。

System.FormatException:

s 的格式不正确。

System.OverflowException:

s 表示小于 System.Int32.MinValue 或大于 System.Int32.MaxValue 的数字

bool TryParse(string s, outint result);

摘要:将数字的字符串表示形式转换为它的等效 32 位有符号整数。一个指示转换是否成功的返回值。

参数:

result:

当此方法返回时,如果转换成功,则包含与 s 所包含的数字等效的 32 位有符号整数值;如果转换失败,则包含零。如果 s 参数为 null,格式不正确,或者表示的数字小于System.Int32.MinValue 或大于System.Int32.MaxValue,则转换会失败。该参数未经初始化即被传递。

返回结果:

如果 s 转换成功,则为 true;否则为 false。

string s = "9999";

int result;

if (int.TryParse(s, out result))

{

HelperJs.MessageBox("成功s=" + result);

}

2.15 封装

面向对象的封装就是把实物的状态和行为封装在类中,使用类的人不需要知道类的内部是怎么实现的,只要调用其中的属性和方法实现功能就行

类和对象本身的就是封装的体现,属性封装了字段;

? 封装的优点:

保证数据的安全性

提供清晰的对方借口

类内部实现可以任意修改,不影响其他类

2.16 继承

继承的特点

C#中只允许单继承,即一个派生类只能有一个基类

C#中继承是可以传递的,如果C从B派生,B从A派生,那么C不仅继承B的成员,而且还继承A的成员,传递性;

C#派生类可以添加新成员,但不能删除基类的成员

C#派生类不能继承基类的构造函数和析构函数,但能继承基类的属性

C#派生类可以隐藏基类的同名成员.如果在派生类可以隐藏基类的同名成员,基类该成员在派生类中就不能直接访问,只能通过”base.基类方法名”来访问

C#派生类的对象也是基类的对象,但基类的对象却不一定是基类派生类的对象.也就是说,基类的引用变量可以引用基类派生类的对象,而派生类的引用变量不可以引用基类对象

所有类都直接或者间接的继承至object类

创建子类对象的时候,先调用子类构造函数,再先调用父类的构造函数,执行完父类的构造函数之后,再执行子类的构造函数;子类的构造函数默认会调用父类的无参数的构造函数;子类构造函数如何调用父类构造函数:public Student (string name , int age):base (name,age) { }如果在子类的构造函数里如果我们不去显示的去指定调用父类的那一个构造函数,那么默认的会去调用父类的无参数的构造函数。

静态类是密封类不能被继承

创建子类对象时,父类的私有成员也会被创建在这个空间里,只是子类访问不到而已

继承的好处:减少代码冗余,方便复用,方便管理,便于修改

子类的访问级别不能比父类的高

如果子类拥有与父类重名的属性和方法的时候,子类的属性和方法会将父类的覆盖掉。如果需要显示调用父类的成员,则需要使用base.不然会默认调用this.

创建一个子类对象,默认先会调用父类的无参的构造函数。如果父类没有无参的构造函数,会报错。

创建子类对象的时候,也可以指定调用父类的带参的构造函数。Base

子类会继承父类的所有成员,但是只能使用公共的,而私有成员不能使用。

继承有三大特性:

单根性:一个类只能继承自另外一个类

传递性:一个类是另外一个类的子类,也可能同时是另外一个子类的父类

单向性:不能循环依赖

里氏替换原则:

1) 子类可以替换父类的位置,并且程序功能不受影响

2) 子类和父类存在同名成员的时候调用的是哪个成员?

父类 obj=new student(); //如果父类和子类存在同名成员时调用父类的成员

父类 obj=new student();//如果父类和子类存在不同名成员时根据类型指针先指向声明的类,如果类中没有则继续寻找它的父类;父类变量不能访问子类中特有的变量和方法

子类 obj=new student();//obj调用的是父类和子类的,如果是同名的则调用子类的

3) new关键字的第二个作用,显示隐藏父类成员

如果子类与父类存在同名成员,子类会将父类的同名成员隐藏掉,通过new关键字显式隐藏

4) 根据变量类型判断可以调用的方法,如果子类里的方法与父类的方法在方法名相同且参数不同的情况下可以构成重载,根据所传的参数确定调用的方法

派生类的声明(子类的声明)

[类修饰符] class 派生类 :基类;

C#派生类可以从它的基类中继承字段/属性/方法/事件/索引器等,实际上除了构造函数和析构函数,派生类隐式地继承了基类的所有成员;

派生类将获取基类的所有的非私有数据和行为;

按次序调用构造函数和析构函数

? 调用默认构造函数的次序

如果类是从一个基类派生出来的,那么在调用这个派生类的默认构造函数之前会调用基类的默认构造函数.调用的此事是从最远的基类开始;

按照这中调用顺序,C#能够保证在调用派生类的构造函数之前,把派生类所需的资源全部准备好;当执行派生类构造函数时,基类的所有字段都初始化了;

? 调用默认析构函数的次序

当销毁对象时,它会按照相反的顺序来调用析构函数;

首先调用派生类的析构函数,然后是最近基类的析构函数,最后才调用那个最远的析构函数;

注意:一个类只能有一个析构函数

? 调用重载构造函数的次序

调用基类重载的构造函数需使用base关键字;base关键字主要是为派生类调用基类成员提供一个简写的方法,可以在子类中使用base关键字访问基类成员.调用基类中重载构造函数的方法是将派生类的重载构造函数作如下设计:

Public 派生类名( 参数列表1 ):base( 参数列表2 )

{

}

其中,”参数列表2”和”参数列表1”存在对应关系

同样,在通过”参数列表1”创建派生类的实例对象时,先以”参数列表2”调用基类的构造函数,再调用派生类的构造函数;

使用sealed修饰符来禁止继承

C#提供了sealed关键字用来禁止继承.要禁止继承一个类,只需要在声明类时加上sealed关键字就可以了,这样的类被称为密封类

Sealed class 类名 { … }

这样就不能从该类派生任何子类

2.17 多态性

所谓多态性,就是同一签名具有不同的表现行为,运算符重载和函数都属于多态的表现形式,本节介绍采用虚方法实现多态性;

同一种操作,多种不同的响应

好处:易于维护,可扩展性;

把不同的子类对象都当做父类来看,可以屏蔽不同的子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求不断的变化

不同的事物对同一个行为作出不同的变现方式--多态

? 如何实现多态:

基于继承关系

子类重写父类的虚方法

父类的变量指向子类的变量

将父类作为参数 实际上传递是子类对象

工厂方法负责生产不同的子类,返回值是1个父类类型

声明一个父类的变量,不关心具体创建的是哪个子类对象

? 实现多态的方式:

通过父类实现多态

通过抽象实现多态

隐藏基类方法

C#若要更改基类的数据和行为,有两种使用新的派生成员代替基成员或者可以重写虚拟的基成员.本小节介绍第一种:

在使用新的派生方法代替基方法时应使用new关键字;例如:

Class A {

Public void fun() { … }

}

Class B : A {

New public void fun() //隐藏基类方法fun

{

}

}

重写

重写是指在子类中编写具有相同名称和参数的方法.重写和重载是不同的,重载是指编写(在同一个类中)具有相同的名称,却有不同参数的方法;而重写,是指子类中的方法和基类中的方法具有相同的签名.而重载的方法具有不同的签名;

? Virtual 关键字

Virtual 关键字用来修饰方法/属性/索引器或事件声明,并且允许在派生类中重写这些对象。只有被标记为virtual的方法才能被类重写,子类可以重写也可以不重写。

例如,以下定义了一个虚拟方法并可以被任何继承它的类重写:

Public virtual double Area()

{

Reutn x * y;

}

调用虚方法时,首先调用派生类中的该重写成员,如果没有派生类重写该成员,则它可能是原始成员.

注意:默认情况下,方法是非虚拟的,不能重写非虚方法

Virtual 修饰符不能与static/abstract和override修饰符一起使用.在静态属性上使用virtual修饰符是错误的;

? 重写方法

Override方法提供从基类继承的成员的新实现.通过override声明的重写方法称为重写基方法.重写基方法必须与oberride方法具有相同的签名.

不能冲洗非虚方法或静态方法.重写的基方法必须是virtual/abstract或override的;

Override声明不能更改virtual方法的可访问性;

不能使用修饰符new/static/virtual或abstact来修改override方法;

重写属性必须制定与继承属性完全相同的访问修饰符/类型和名称,并且被重写的属性必须是virtual/abtract或overrider的。

如果子类重写了父类中的虚方法,调用的时候当发现父类的方法表中的这个方法是虚方法那么就会去子类中寻找重写的方法,若重写了则调用,若子类中没有重写方法,则还是调用父类的虚方法;如果C:B B:A如果在对空间创建的C的对象,那么调用虚方法时,会先去找C是否重写,如果重写了,就调用C的重写方法,如果没有重写再看B是否重写 如果重写调用B重写的方法 如果B没有重写,那么直接调用A的虚方法。

函数必须相同(返回值和函数名和参数列表)

重写的函数修饰符必须大于父类

重写的函数返回值必须与父类一致

构造函数无法重写,不能被virtual修饰,不能继承

被sealed修饰的类叫做密封类,密封类是不能被继承的,不希望被继承的时候可以将类定义为密封类,被sealed修饰的重写的方法不能继续被其子类再重写,不能修饰字段,可以修饰类和方法

虚方法不能被private访问修饰符修饰

equals方法:

1) 是定义在object里面的一个虚方法,如果是值类型的比较的是变量的值,如果是引用类型,比较的是2个对象的引用地址,如果需要修改equals的比值方法可以在子类中重写改方法

2) tostring也可以重写

补充说明:

声明父类变量,实例化子类对象

父类创建虚方法,子类做重写。---子类重写父类的方法

虚方法提供了默认的实现,子类可以不重写,如果子类没有重写,那么就默认调用父提供的方法实现。如果子类重写了,系统会自动判断子类类型,调用子类的重写方法 -----这就是多态

多态的核心在于:子类可以替换父类,原因:

子类拥有父类所规范的所有属性和方法

子类还可以拥有自己特定的属性和方法

父类对象可以完成的功能子类对象都可以完成

所以,子类可以替换父类。如Person per=new Student();

Per本质是一个子类对象,但是编译器不知道,所以per只能调用父类里面规范的成员。如果做强制转换,则可以调用到子类成员。

如果需要系统自动判断类型,达到多态的目的需要做以下事情:

实现继承---- 子类是父类

父类中声明虚方法(抽象方法),子类做重写(override)

声明父类变量,实例化子类对象 / 父类做为参数,传入子类对象。

多态实现了避免反复的判断才能达到的效果,这也是多态的好处。方便以后的代码功能的扩展

抽象方法:abstract:只是方法的声明,没有方法体,不能包含{},但是必须包含 ;

抽象方法的特点 :

只有声明

它必须在抽象类里面(抽象类里面不仅仅只包含抽象方法)

所有子类必须重写父类的抽象方法,除非子类它也是抽象的,抽象类里面可以包含未实现的抽象方法。

抽象方法更多是一种规范,它规范了子类的行为。---接口

抽象类不能实例化。

多态的使用方式:

声明父类变量,实例化子类对象 ---创建父类类型集合

以父类做为方法的返回值,返回子类对象---简单工厂:用户需要频繁的得到不同的单个子类对象的时候

以父类类型做为参数,传入子类对象

如果父子类的方法参数不一样,那它是重载而不是重写。

.重写要求方法的签名完全一致:访问类型一样,返回值类型一样,方法名称一样,方法的参数一样,只是方法体的实现过程不一样

2.18 抽象类

在类声明中使用abstract修饰符修饰的类被称为抽象类;抽象类具有以下特点:

l 抽象类不能被实例化

l 抽象类可以包含抽象方法和抽象访问器

l 抽象类中还可以存在非抽象方法

l 不能使用sealed修饰符修改抽象类,这也意味着抽象类不能被继承

l 从抽象类派生的非抽象类必须包括继承的所有抽象方法和抽象访问器的实现

l 抽象类可以被抽象类所继承,结果仍是抽象类

l 抽象方法不能有实现,方法体;也就是不能有大括弧;分号写在参数后面

l 抽象方法必须在抽象类中,抽象类可以有构造函数

l 抽象类不能被实例化,子类继承抽象类的时候必须重写父类的抽象方法;因为抽象方法没有方法体,实例化没有意义

l 子类继承抽象类,必须重写父类所有的抽象方法,除非你本身也是一个抽象类

l 抽象方法不能是私有的不能用private修饰,虚方法也不能被private修饰

l 抽象类里能有非抽象成员,这些非私有的成员照样被子类继承

? 抽象方法与虚方法不一样的地方:

抽象方法:abstract,没有方法体,必须在抽象类中;

虚方法:virtual,必须有方法体,可以在抽象类中,不能在密封类中

? 什么时候需要用到抽象方法:

每一个子类的方法实现都不一样,但是都有这个方法,可以用抽象方法

a.父类必须要求子类重写父类的抽象方法

b.父类没有必要实例化的时候就用抽象类

? 什么时候需要用到虚方法?

看这个方法是否有默认的实现,是否有默认的实行代码;子类的方法体是不是有的和父类一样,若是一样则可以用虚方法

? 定义抽象类注意以下准则:

l 不要在抽象类中定义公共的或受保护的内部构造函数.具有public或protected inernal可见性的构造函数用于能进行实例化的类型.任何情况下抽象类都不能实例化

l 应在抽象类中定义一个受保护构造函数或内部构造函数;如果在抽象类中定义一个受保护构造函数,则在创建派生类的实例时,基类可执行初始化任务;内部构造函数可防止抽象类被用做其他程序集中的类型的基类;

抽象方法

在方法声明中使用abstract修饰符以指示方法不包含实现的,即为抽象方法;抽象方法具有以下特性:

l 声明一个抽象方法使用abstract关键字

l 抽象方法是隐式的虚方法

l 只允许在抽象类中使用抽象方法声明

l 一个类中可以包含一个或多个抽象方法

l 因为抽象方法声明不提供实际的实现,所以没有方法体;方法声明只是一个分号结束,并且在签名后没有大括号{}

l 抽象方法实现有一个重写方法提供,此重写方法是非抽象类的成员

l 实现抽象类用”:”,实现抽象方法用override关键字

l 在抽象方法声明中使用static或virtual修饰符是错误的

l 抽象方法被实现后,不能更改修饰符

如果一个类包含任何抽象方法,那么该类本身必须被标记为抽象类

抽象属性

除了在声明和调用语法上不同外,抽象属性的行为与抽象方法类似,另外,抽象属性具有以下特性:

l 在静态属性上使用abstract修饰符是错误的

l 在派生类中,通过包括使用override修饰符的属性声明,可以重写抽象的继承属性

l 抽象属性声明不提供属性访问器的实现,它只声明该类支持属性,而将访问器实现留给其派生类

例如:

using System;

namespace ConsoleApplication2

{

publicdelegatevoidmydelegate();

classProgram

{

abstractclassA//抽象类声明

{

protectedint x = 2;

protectedint y = 3;

publicabstractvoid fun();//抽象方法声明

publicabstractint px { get;set; } //抽象属性声明

publicabstractint py { get; } //抽象属性声明,只读

}

classB:A

{

publicoverridevoid fun() //抽象方法实现

{

base.x++;

base.y++;

}

publicoverrideint px //抽象属性实现

{

get

{

returnbase.x + 10;

}

set

{

base.x = value;

}

}

publicoverrideint py

{

get { return y + 10; }

}

}

staticvoid Main(string[] args)

{

B b = newB();

b.px = 5;

b.fun();

Console.WriteLine("x={0},y={1}", b.px, b.py);

Console.ReadKey();

}

}

}

2.19 接口

什么是接口

接口是类之间交互内容的一个抽象,把类之间需要交互的内容抽象出来定义成一个接口,可以更好的控制类之间的逻辑交互;接口具有下列特性:

l 接口类似于抽象基类;继承接口的任何非抽象类型都必须实现接口的所有成员;

l 不能直接实例化接口

l 接口可以包含事件/索引器/方法和属性

l 接口不包含方法的实现

l 类和结构可从多个接口继承

l 接口本身可从多个接口继承

接口只包含成员定义,不包含成员的实现,成员的实现需要在继承的类或者结构中实现.接口的成员包括方法/属性/索引器和事件,但不包含字段;

接口和抽象类在定义上和功能有很多类似的地方,但两者之间也存在差异.接口是最适合为不相关的类提供通用功能.通常在设计小而简练的功能块时使用接口;

1) 声明接口的关键字:interface

interface IRunable接口的名称首字母必须是大写I

2) 接口不能有字段(只能有方法属性索引器和事件);可以有方法,方法不能有方法体,不能有访问修饰符

3) 接口里的成员的访问修饰符默认就是public,如果实现了接口,必须实现接口里面所有的属性和方法

4) 接口的作用:为了完全的约束子类的行为

5) 接口的实现类似于继承

6) 一个类可以同时集成一个类并实现多个接口

若接口名与父类名称同时跟在:后面,继承的类要写在最前面,因为继承只能单继承而接口可以多实现

7) 显示实现接口:

如果是显示实现接口,那么这个实现类的对象是调用不到这个实现方法的,通过接口才可以调用;显示实现方法不能有访问修饰符

8) 实现类可以将接口中的方法,实现为虚方法也可以实现为抽象方法,供子类重写

9) 接口中的成员不能有任何实现(光说不做)

接口的定义

? 接口的声明

一个接口声明属于一个类型说明,语法格式如下:

[接口修饰符] interface 接口名 [:父接口列表]

{

//接口成员定义体

}

其中,接口修饰符可以是new/public/protected/internal和private;new 修饰符是在嵌套接口中唯一被允许存在的修饰符,表示用相同的名称隐藏一个继承的成员;

? 接口的继承

可以可以从0个或多个接口中继承;当一个接口从多个接口中继承是,用”:”后跟被继承的接口名称,这多个接口之间用”,”号分隔.被继承的接口应该是可以被访问的,即能从internal或internal类型的接口继承;

对一个接口继承也就继承了接口的所有成员;

接口的成员

接口可以声明0个或多个成员;一个接口的成员不止包括自身声明的成员,还包括从父接口继承的成员;所有接口成员默认都是公有的,接口成员声明中包含任何修饰符都是错误的;

? 接口方法成员

返回类型 方法名([参数表]);

? 接口属性成员

返回类型 属性名{get ;或 set;};

? 接口索引器成员

数据类型this[索引参数表] {get;或 set;};

? 接口事件成员

Event代表名 事件名;

例如:

Public delegate void mydegegate(); //声明一个委托类型

Public interface Ia

{

Event mydelegate myevent;

}

接口的实现

接口的实现分为隐式实现和显式实现;如果类或者结构要实现的是单个接口,可以使用隐式实现,如果类或者结构继承了多个接口,那么接口中相同名称成员就要显式实现;显式实现是通过使用接口的完全限定名来实现接口成员的;

接口实现的语法格式:

Class 类名 :接口名列表

{

//类实体

}

注意:

当一个类实现一个接口时,这个类就必须实现整个接口,而不能选择实现接口的某一部分

一个接口可以由多个类来实现,而在一个类中也可以实现一个或多个接口.

一个类可以继承一个基类,并同事实现一个或多个接口;

? 隐式实现接口成员

如果类实现了某个接口,它必然隐式地继承了该接口成员,只不过增加了该接口成员的具体实现;若要隐式实现接口成员,类中的对应成员必须是公共的的/非静态的,并且与接口成员具有相同的名称和签名;

一个接口可以被多个类继承,在这些类中实现该接口的成员,这样接口就起到提供统一界面的作用;

? 显式实现接口成员

当类实现接口时,如给出了接口成员的完整名称即带有接口名前缀,则称为这样实现的成员为显式接口成员,其实现被称为显式接口实现;显示接口实现不能使用任何修饰符;

显示接口实现时导致隐藏接口成员.所以如果没有充分的理由,应避免显式实现接口成员;如果成员只应通过接口调用,则考虑显式实现接口成员;

接口映射

接口通过类实现,对于接口中声明的每一个成员都应该对应着类的一个成员,这中对应关系是有接口映射来实现的;

…略

接口实现的继承

因为接口中所有成员都是公有的,所以一个类可以从它的基类继承所有的接口实现;除非在派生类中重新实现一个接口,否则派生类将无法改变从基类继承而来的接口映射;

当接口方法被映射到类中的虚方法时,派生类就可以通过覆盖基类虚方法来改变接口的实现;

…略

重写实现接口

所谓接口的重新实现,是指某个类可以通过它的基类中包含已被基类实现的接口再一次的实现该接口;

当重新实现接口时,不管派生类建立的接口映射如何,它从基类继承的接口映射并不会收到影响;

在接口的重实现时,继承而来的公有成员定义和继承而来的显示接口成员的定义都参与到了接口映射的过程;

接口补充

接口本质就是一个抽象类,在接口里面就是声明一系列的属性,方法,索引器和事件。从反编译工具可以看到接口是抽象类---抽象的不能实例化,只能被实现;接口是一个类,说明它也是一种数据类型,可以通过接口创建出接口对象(所谓的接口对象其实是实现该接口的类的对象)

创建一个接口对象,其实就是创建一个 实现了这个接口的类 的对象

如果某个类没有实现某个接口,那么这个这个类的对象不能强制转换为接口类型。

接口不要求实现接口的类有某种关联(只需要有共同的行为),也就是说不要求他们要父子继承关系,方便了用户的拓展。 父类---虚方法/抽象方法---子类重写---多态

在公司里面,接口更多的认为是一种规范。

使用接口的方式:

声明接口类型的变量,存入实现了接口的类的 对象

以接口类型做为参数,传入实现了接口的类的 对象

显示调用接口:显示实现接口的方法需要接口类型的对象才能调用。因类它默认是私有的,而且还不能人为修改为public

如果声明的类型就是接口类型,那么可直接调用

如果声明的类型是实现了接口的类的类型,那么就需要做强制转换。

接口解决了通过继承实现多态的三个问题:

通过继承实现多态只能是单继承,但是接口可以多实现

通过继承实现多态的类必能够提取出共同的父类,也就说明这些类之间需要有共同的特征和行为。但是接口只需要它们有共同的行为。

通过实现了继承的类做为参数传递,会传递类的对象的所有属性和方法,但是通过接口做为参数,只能调用到接口中声明的方法和属性。---更加安全

2.20 接口在集合排序中的应用

2.21 泛型编程

[WuDe]C#程序设计教程 - 第2章 C#面向对象基础

标签:style   blog   http   io   ar   color   os   使用   sp   

原文地址:http://www.cnblogs.com/Albert-Einstein/p/4154993.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!