C#课程总结报告.docx
- 文档编号:4865676
- 上传时间:2022-12-11
- 格式:DOCX
- 页数:12
- 大小:23.66KB
C#课程总结报告.docx
《C#课程总结报告.docx》由会员分享,可在线阅读,更多相关《C#课程总结报告.docx(12页珍藏版)》请在冰豆网上搜索。
C#课程总结报告
知识点1:
引用和值类型总结
从概念上看,值类型直接存储其值,而引用类型存储对其值的引用。
这两种类型存储在内存的不同地方。
在C#中,我们必须在设计类型的时候就决定类型实例的行为。
这种决定非常重要,用《CLRviaC#》作者JeffreyRichter的话来说,“不理解引用类型和值类型区别的程序员将会给代码引入诡异的bug和性能问题(Ibelievethatadeveloperwhomisunderstandsthedifferencebetweenreferencetypesandvaluetypeswillintroducesubtlebugsandperformanceissuesintotheircode.)”。
这就要求我们正确理解和使用值类型和引用类型。
1.通用类型系统
C#中,变量是值还是引用仅取决于其数据类型。
C#的基本数据类型都以平台无关的方式来定义。
C#的预定义类型并没有内置于语言中,而是内置于.NETFramework中。
.NET使用通用类型系统(CTS)定义了可以在中间语言(IL)中使用的预定义数据类型,所有面向.NET的语言都最终被编译为IL,即编译为基于CTS类型的代码。
例如,在C#中声明一个int变量时,声明的实际上是CTS中System.Int32的一个实例。
这具有重要的意义:
确保IL上的强制类型安全;
实现了不同.NET语言的互操作性;
所有的数据类型都是对象。
它们可以有方法,属性,等。
例如:
inti;
i=1;
strings;
s=i.ToString();
MSDN的这张图说明了CTS中各个类型是如何相关的。
注意,类型的实例可以只是值类型或自描述类型,即使这些类型有子类别也是如此。
2.值类型
C#的所有值类型均隐式派生自System.ValueType:
结构体:
struct(直接派生于System.ValueType);
数值类型:
整型:
sbyte(System.SByte的别名),short(System.Int16),int(System.Int32),long(System.Int64),byte(System.Byte),ushort(System.UInt16),uint(System.UInt32),ulong(System.UInt64),char(System.Char);
浮点型:
float(System.Single),double(System.Double);
用于财务计算的高精度decimal型:
decimal(System.Decimal)。
bool型:
bool(System.Boolean的别名);
用户定义的结构体(派生于System.ValueType)。
枚举:
enum(派生于System.Enum);
可空类型(派生于System.Nullable
实际上是System.Nullable
每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值。
例如:
inti=newint();
等价于:
Int32i=newInt32();
等价于:
inti=0;
等价于:
Int32i=0;
使用new运算符时,将调用特定类型的默认构造函数并对变量赋以默认值。
在上例中,默认构造函数将值0赋给了i。
MSDN上有完整的默认值表。
关于int和Int32的细节,在我的另一篇文章中有详细解释:
《理解C#中的System.Int32和int》。
所有的值类型都是密封(seal)的,所以无法派生出新的值类型。
值得注意的是,System.ValueType直接派生于System.Object。
即System.ValueType本身是一个类类型,而不是值类型。
其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。
可以用Type.IsValueType属性来判断一个类型是否为值类型:
复制代码代码如下:
TestTypetestType=newTestType();
if(testTypetype.GetType().IsValueType)
{
Console.WriteLine("{0}isvaluetype.",testType.ToString());
}
3.引用类型
C#有以下一些引用类型:
数组(派生于System.Array)
用户用定义的以下类型:
类:
class(派生于System.Object);
接口:
interface(接口不是一个“东西”,所以不存在派生于何处的问题。
Anders在《C#ProgrammingLanguage》中说,接口只是表示一种约定[contract]);
委托:
delegate(派生于System.Delegate)。
object(System.Object的别名);
字符串:
string(System.String的别名)。
可以看出:
引用类型与值类型相同的是,结构体也可以实现接口;
引用类型可以派生出新的类型,而值类型不能;
引用类型可以包含null值,值类型不能(可空类型功能允许将null赋给值类型);
引用类型变量的赋值只复制对对象的引用,而不复制对象本身。
而将一个值类型变量赋给另一个值类型变量时,将复制包含的值。
对于最后一条,经常混淆的是string。
我曾经在一本书的一个早期版本上看到String变量比string变量效率高;我还经常听说String是引用类型,string是值类型,等等。
例如:
strings1="Hello,";
strings2="world!
";
strings3=s1+s2;//s3is"Hello,world!
"
这确实看起来像一个值类型的赋值。
再如:
strings1="a";
strings2=s1;
s1="b";//s2isstill"a"
改变s1的值对s2没有影响。
这更使string看起来像值类型。
实际上,这是运算符重载的结果,当s1被改变时,.NET在托管堆上为s1重新分配了内存。
这样的目的,是为了将做为引用类型的string实现为通常语义下的字符串。
4.总结
C#中,变量是值还是引用仅取决于其数据类型。
C#的值类型包括:
结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型。
C#的引用类型包括:
数组,用户定义的类、接口、委托,object,字符串。
数组的元素,不管是引用类型还是值类型,都存储在托管堆上。
引用类型在栈中存储一个引用,其实际的存储位置位于托管堆。
为了方便,本文简称引用类型部署在托管推上。
值类型总是分配在它声明的地方:
作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。
值类型在内存管理方面具有更好的效率,并且不支持多态,适合用作存储数据的载体;引用类型支持多态,适合用于定义应用程序的行为。
应该尽可能地将值类型实现为具有常量性和原子性的类型。
应该尽可能地确保0为值类型的有效状态。
应该尽可能地减少装箱和拆箱。
知识点2:
封装
可以把程序按某种规则分成很多“块“,块与块之间可能会有联系,每个块都有一个可变部分和一个稳定的部分。
我们需要把可变的部分和稳定的部分分离出来,将稳定的部分暴露给其他块,而将可变的部分隐藏起来,以便于随时可以让它修改。
这项工作就是封装。
例如:
在用类实现某个逻辑的时候,类就是以上所说的块,实现功能的具体代码就是可变的部分,而public的方法或者属性则是稳定的部分。
封装的意义
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
在面向对象程序设计中数据被看作是一个中心的原素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。
封装提供了一个有效的途径来保护数据不被意外的破坏。
相比我们将数据(用域来实现)在程序中定义为公用的(public)我们将它们(fields)定义为私有的(private)在很多方面会更好。
私有的数据可以用两种方式来间接的控制。
下面我们看一些c#例子来学习这两种方法用以封装数据。
第一种方法,我们使用传统的存、取方法。
第二种方法我们用属性(property)。
无论我们使用哪种的方法,我们的目标是在使用数据的同时不能使它受到任何的破坏和改变。
有如下好处
1、使用者只需要了解如何通过类的接口使用类,而不用关心类的内部数据结构和数据组织方法。
2、高内聚,低耦合一直是我们所追求的,用好封装恰恰可以减少耦合
3、只要对外接口不改变,可以任意修改内部实现,这个可以很好的应对变化
4、类具有了简洁清晰的对外接口,降低了使用者的学习过程
用传统的读、写方法封装
让我们来看一个例子有一个类Grade(年级信息类),为了操纵这个类中的数据(stringgradeName(年级名称))我们定义了一个读方法和一个写方法。
usingsystem;
publicclassGrade
{
privatestringgradeName;
.......
//读方法
publicstringGetGradeName()
{
returngradeName;
}
//写方法
publicvoidSetGradeName(stringgradeName)
{
this.gradeName=gradeName;
}
}
通过上面的方法,我们可以保护私有数据不被外部程序所破坏。
现在我们使用两个不同的方法来写和读数据
publicstaticvoidMain(string[]args)
{
Gradegrade=newGrade();
grade.SetGradeName("S2")
Console.WriteLine("TheGradeis:
"+grade.GetGradeName());
}
在上面的例子中,我们不能直接访问类Grade的实例grade中的私有数据(stringgradeName),我们只能通过这两个方法来访问。
用属性来实现封装
属性是c#引入的一种语言成分,只有很少的语言支持属性。
通过对属性的读和写来保护类中的域。
第一种方法体身也是一种好的方式,但用属性来实现封装会更方便。
现在我们来看一个例子:
usingsystem;
publicclassGrade
{
privatestringgradeName;
publicstringGradeName
{
get
{
returngradeName;
}
set
{
gradeName=value;
}
}
}
publicstaticvoidMain(string[]args)
{
Gradegrade=newGrade();
grade.GradeName="S2";
Console.WriteLine("TheGradeis:
{0}",grade.GradeName);
}
通过上面的例子,我们可以看到如何通过属性来实现封装。
属性具有两种操作get和set。
get用来返回属性域的值。
set通过value这个变量来给属性域赋值。
属性可以设为只读的(read-only)。
这只需属性只具有一个set操作。
只读属性
publicstringGradeName
{
get
{
returngradeName;
}
}
在上面的例子中我们看到了如何来实现一个只读的属性。
类Grade拥有一个GradeName属性只实现了get操作。
它省略了写操作。
这个特别的类拥有一个构造器,用来接受一个字符串变量。
在Main方法创建了一个新的对象grade。
对象grade的实例使用了类Grade带有一个字符串参数的构造器。
因为上面的属性是只读的,所以我们不给域gradeName赋值并且我们只侧读取此域中的值。
当然属性也可以是只写的(write-only),这只需属性只具有一个get操作。
publicstringGradeName
{
set
{
this.gradeName=value;
}
}
在上面的例子中我们看到了如何来实现一个只写的属性。
类Grade拥有一个GradeName属性只实现了set操作。
它省略了读操作。
总结
封装是朝着面向对象程序设计迈出的第一步。
本文主要展示了一些封装的知识。
用传统的读、写两种方法可以实现封装,另一种实现封装的方法是使用属性。
使用属性的好处在于对象的使用者可以用一条语句来操作内部的数据
知识点3:
继承
一、继承的定义
二、
为了提高软件模块的可复用性和可扩充性,以便提高软件的开发效率,我们总是希望能够利用前人或自己以前的开发成果,同时又希望在自己的开发过程中能够有足够的灵活性,不拘泥于复用的模块。
C#这种完全面向对象的程序设计语言提供了两个重要的特性--继承性inheritance和多态性polymorphism。
继承是面向对象程序设计的主要特征之一,它可以让您重用代码,可以节省程序设计的时间。
继承就是在类之间建立一种相交关系,使得新定义的派生类的实例可以继承已有的基类的特征和能力,而且可以加入新的特性或者是修改已有的特性建立起类的新层次。
现实世界中的许多实体之间不是相互孤立的,它们往往具有共同的特征也存在内在的差别。
人们可以采用层次结构来描述这些实体之间的相似之处和不同之处。
为了用软件语言对现实世界中的层次结构进行模型化,面向对象的程序设计技术引入了继承的概念。
一个类从另一个类派生出来时,派生类从基类那里继承特性。
派生类也可以作为其它类的基类。
从一个基类派生出来的多层类形成了类的层次结构。
注意:
C#中,派生类只能从一个类中继承。
这是因为,在C++中,人们在大多数情况下不需要一个从多个类中派生的类。
从多个基类中派生一个类这往往会带来许多问题,从而抵消了这种灵活性带来的优势。
C#中,派生类从它的直接基类中继承成员:
方法、域、属性、事件、索引指示器。
除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员
二、C#中的继承符合下列规则:
1、继承是可传递的。
如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。
Object类作为所有类的基类。
2、派生类应当是对基类的扩展。
派生类可以添加新的成员,但不能除去已经继承的成员的定义。
3、构造函数和析构函数不能被继承。
除此以外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。
基类中成员的访问方式只能决定派生类能否访问它们。
4、派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员。
但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。
5、类可以定义虚方法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。
6、派生类只能从一个类中继承,可以通过接吕实现多重继承。
下面的代码是一个子类继承父类的例子:
三.访问与隐藏基类成员
(1)访问基类成员
通过base关键字访问基类的成员:
调用基类上已被其他方法重写的方法。
指定创建派生类实例时应调用的基类构造函数。
基类访问只能在构造函数、实例方法或实例属性访问器中进行。
从静态方法中使用base关键字是错误的。
(2)隐藏基类成员
想想看,如果所有的类都可以被继承,继承的滥用会带来什么后果?
类的层次结构体系将变得十分庞,大类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。
有时候,我们并不希望自己编写的类被继承。
另一些时候,有的类已经没有再被继承的必要。
C#提出了一个密封类(sealedclass)的概念,帮助开发人员来解决这一问题。
密封类在声明中使用sealed修饰符,这样就可以防止该类被其它类继承。
如果试图将一个密封类作为其它类的基类,C#将提示出错。
理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。
在哪些场合下使用密封类呢?
密封类可以阻止其它程序员在无意中继承该类。
而且密封类可以起到运行时优化的效果。
实际上,密封类中不可能有派生类。
如果密封类实例中存在虚成员函数,该成员函数可以转化为非虚的,函数修饰符virtual不再生效。
(3)密封方法
我们已经知道,使用密封类可以防止对类的继承。
C#还提出了密封方法(sealedmethod)的概念,以防止在方法所在类的派生类中对该方法的重载。
对方法可以使用sealed修饰符,这时我们称该方法是一个密封方法。
不是类的每个成员方法都可以作为密封方法密封方法,必须对基类的虚方法进行重载,提供具体的实现方法。
所以,在方法的声明中,sealed修饰符总是和override修饰符同时使用。
(4)使用new修饰符隐藏基类成员
使用new修饰符可以显式隐藏从基类继承的成员。
若要隐藏继承的成员,请使用相同名称在派生类中声明该成员,并用new修饰符修饰它。
通过继承隐藏名称采用下列形式之一:
a、引入类或结构中的常数、指定、属性或类型隐藏具有相同名称的所有基类成员。
b、引入类或结构中的方法隐藏基类中具有相同名称的属性、字段和类型。
同时也隐藏具有相同签名的所有基类方法。
c、引入类或结构中的索引器将隐藏具有相同名称的所有基类索引器。
注意:
在同一成员上同时使用new和override是错误的。
同时使用new和virtual可保证一个新的专用化点。
在不隐藏继承成员的声明中使用new修饰符将发出警告。
四、多级继承
一些面向对象语言允许一个类从多个基类中继承,而另一些面向对象语言只允许从一个类继承,但可以随意从几个接口或纯抽象类中继承。
只有C++支持多级继承,许多程序员对此褒贬不一。
多级继承常会引起继承来的类之间的混乱,继承而来的方法往往没有唯一性,所以C#中类的继承只可以是一个,即子类只能派生于一个父类,而有时你必须继承多个类的特性,为了实现多重继承必须使用接口技术。
五、继承与访问修饰符
访问修饰符是一些关键字,用于指定声明的成员或类型的可访问性。
类的继承中有四个访问修饰符:
publicprotectedinternalprivate。
使用这些访问修饰符可指定下列五个可访问性级别:
publicprotectedinternalinternalprotectedprivate。
声明的可访问性
意义
public
访问不受限制。
protected
访问仅限于包含类或从包含类派生的类型。
internal
访问仅限于当前项目。
protectedinternal
访问仅限于从包含类派生的当前项目或类型。
private
访问仅限于包含类型。
1、继承中关于可访问域的一些问题
基类的所有成员(实例构造函数、析构函数和静态构造函数除外)都由派生类型继承。
这甚至包括基类的私有成员。
但是,私有成员的可访问域只包括声明该成员的类型的程序文本。
2、继承中关于属性的一些问题
和类的成员方法一样,我们也可以定义属性的重载、虚属性、抽象属性以及密封属性的概念。
与类和方法一样,属性的修饰也应符合下列规则:
属性的重载
1.在派生类中使用修饰符的属性,表示对基类中的同名属性进行重载。
2.在重载的声明中,属性的名称、类型、访问修饰符都应该与基类中被继承的属性一致。
3.如果基类的属性只有一个属性访问器,重载后的属性也应只有一个。
但如果基类的属性同时包含get和set属性访问器,重载后的属性可以只有一个,也可以同时有两个属性访问器。
注意:
与方法重载不同的是,属性的重载声明实际上并没有声明新的属性,而只是为已有的虚属性提供访问器的具体实现。
虚属性
1.使用virtual修饰符声明的属性为虚属性。
2.虚属性的访问器包括get访问器和set访问器,同样也是虚的。
抽象属性
1.使用abstract修饰符声明的属性为抽象属性。
2.抽象属性的访问器也是虚的,而且没有提供访问器的具体实现。
这就要求在非虚的派生类中,由派生类自己通过重载属性来提供对访问器的具体实现。
3.abstract和override修饰符的同时使用,不但表示属性是抽象的,。
而且它重载了基类中的虚属性这时属性的访问器也是抽象的。
4.抽象属性只允许在抽象类中声明。
5.除了同时使用abstract和override修饰符这种情况之外,static,virtual,override和abstract修饰符中任意两个不能再同时出现。
密封属性
1.使用sealed修饰符声明的属性为密封属性。
类的密封属性不允许在派生类中被继承。
密封属性的访问器同样也是密封的。
2.属性声明时如果有sealed修饰符,同时也必须要有override修饰符。
从上面可以看出,属性的这些规则与方法十分类似。
对于属性的访问器,我们可以把get访问器看成是一个与属性修饰符相同、没有参数、返回值为属性的值类型的方法,把set访问器看成是一个与属性修饰符相同、仅含有一个value参数、返回类型为void的方法。
3、继承中对使用可访问性级别的限制
声明类型时,最重要的是查看该类型是否必须"至少"与其他成员或类型"具有同样的可访问性"。
例如,直接基类必须至少与派生类具有同样的可访问性。
下表汇总了对使用声明的可访问性级别的限制。
上下文
备注
类
类类型的直接基类必须至少与类类型本身具有同样的可访问性。
接口
接口类型的显式基接口必须至少与接口类型本身具有同样的可访问性。
委托
委托类型的返回类型和参数类型必须至少与委托类型本身具有同样的可访问性。
常数
常数的类型必须至少与常数本身具有同样的可访问性。
字段
字段的类型必须与至少字段本身具有同样的可访问性。
方法
方法的返回类型和参数类型必须至少与方法本身具有同样的可访问性。
属性
属性的类型必须至少与属性本身具有同样的可访问性。
事件
事件的类型必须至少与事件本身具有同样的可访问性。
索引器
索引器的类型和参数类型必须至少与索引器本身具有同样的可访问性。
运算符
运算符的返回类型和参数类型必须至少与运算符本身具有同样的可访问性。
构造函数
构造函数的参数类型必须至少与构造函数本身具有同样的可访问性。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C# 课程 总结报告