.NET基础 (04)基础类型和语法
基础类型和语法
1 .NET中所有内建类型的基类是什么
2 System.Object中包含哪些方法,哪些是虚方法
3 值类型和引用类型的区别
4 简述装箱和拆箱原理
5 C#中是否有全局变量
6 struct和class的区别
7 类型的初始化器何时被调用
8 C#中方法的参数可以有哪几种传递方式
9 C#中string和String有什么区别
10 .NET支持哪几种可访问性级别,C#实现了其中的哪几种
11 简述属性的特点及属性和方法的异同
12 简述C#中的浅复制和深复制
13 简述C#中的循环语法和各自的特点
14 C#中的using语句有什么作用
.NET中所有内件类型都继承自System.Object类型。在C#中,不需要显式地定义类型继承自System.Object,编译器将自动地为类型添加上这个继承申明。一下两行C#代码是完全一致的:
public class A{} public class A:System.Object{}
公共实例方法:
virtual bool Equals(object obj)
确定两个对象是否相等,如果相等返回true,不相等返回false
这个方法的实现就是简单比较this引用和参数引用是否引用了同一个对象,如果是则返回true,否则返回false。这个比较通常被称为引用比较,而且不实用。当我们拥有a和b两个字符串,分别为它们赋相同的值,在通常的逻辑下这两个变量相等,但按照System.Object的Equals的实现,这个比较结果是false。所以,当程序员设计类时,可以考虑重写该方法来实现比较内容而非引用。System.ValueType作为所有值类型的基类,重写了Equals方法并且实现了内容的比较。
vairual int GetHashCode()
返回this对象的一个哈希码,如果一个对象被一个集合当做键值使用,则这个值会起作用。
通常,程序员不会重写GetHashCode方法,因为提供一个正确并且高效的GetHashCode算法是一件非常困难的事情。
Type GetType()
返回当前对象的类型。类型由一个继承自System.Type的实例对象表示。
virtual string ToString()
返回一个可以代表当前对象的字符串,在System.Object中该方法将返回当前对象类型的完整名称。
通常,在设计一个类型时都考虑重写ToString方法,让其输出一个有意义的、能完整反映对象内容的字符串。
公共静态方法:
bool Equals(object objA,object objB)
确定两个对象是否相等,如果相等则返回true,不相等则返回false。
它的实现完全依靠参数成员的实例Equals方法。它的实现类似于如下代码:
public static bool Equals(object left, object right) { //查看是否是同一对象 if (left == right) { return true; } if ((left == null) || (right == null)) { return false; } return left.Equals(right); }
bool ReferenceEquals(object objA,object objB)
比较两个对象的引用是否相等,如果相等则返回true,不相等则返回false。
用ReferenceEquals比较两个值类型对象,无论是何种值类型对象,所有返回结果必定是false。
受保护实例方法:
object MemberwiseClone()
浅复制当前对象实例,并返回复制对象的引用
Finalize
.NET的析构方法。
所有.NET的类型都可以分为两类:值类型和引用类型。所有的值类型都继承自System.ValueType(System.ValueType继承自System.Object)。常用的值类型包括结构、枚举、整数型、浮点型、布尔型等,而在C#中所有以class关键字定义的类型都是应用类型。
严格来说,System.Object作为所有内建类型的基类,本身并没有值类型和引用类型之分。但是System.Object的对象,具有引用类型的特点。这也是值类型在有些场合下需要装箱拆箱的原因。
1.赋值的区别
值类型变量将直接获得一个真实的数据副本,而对引用类型的赋值仅仅是把对象的引用赋值给变量,这样可能导致多个变量引用到一个实际对象实例上。
/// <summary> /// 一个简单的引用类型 /// </summary> public class Ref { private int _int; public Ref(int i) { _int = i; } public int I { get { return _int; } set { _int = value; } } public override string ToString() { return "I 的值为:" + _int; } } /// <summary> /// 一个简单的值类型 /// </summary> public struct Val { private int _int; public Val(int i) { _int = i; } public int I { get { return _int; } set { _int = value; } } public override string ToString() { return "I 的值为:" + _int.ToString(); } }
调用:
static void Main(string[] args) { //测试引用类型的赋值 Ref ref1=new Ref(1); Ref ref2 = ref1; ref2.I = 2; //测试值类型的赋值 Val val1=new Val(1); Val val2 = val1; val2.I = 2; //输出 Console.WriteLine("ref1 " + ref1); Console.WriteLine("ref2 " + ref2); Console.WriteLine("val1 " + val1); Console.WriteLine("val2 " + val2); Console.ReadKey(); }
输出:
ref1 I 的值为:2
ref2 I 的值为:2
val1 I 的值为:1
val2 I 的值为:2
2.内存的区别
引用类型的对象会在堆上分配内存,而值类型的对象则会在堆栈上分配内存。堆栈的空间相对有限,但运行效率却比堆高的多。
3.来自继承结构的区别
由于所有值类型都有一个共同的基类System.ValueType,所以值类型拥有一些引用类型不具有的共同性质。值类型的Equals方法的实现有了改变,所有值类型已经实现了内容的比较。
既然System.Object是所有值类型的基类,那么所有的值类型必然可以隐式地转换成System.Object类型。当这个转换发生时,CLR要做额外的工作把堆栈上的值类型移到堆上,这个操作就被称为装箱。步骤如下:
- 在堆上分配一个内存空间,大小等于需要装箱的值类型对象的大小加上两个引用类型对象都拥有的成员:类型对象指针和同步块引用。
- 把堆栈上的值类型对象复制到堆上新分配的对象。
- 返回一个指向堆上新对象的引用,并且存储到堆栈上被装箱的那个值类型的对象里。
这些步骤都不需要程序员自己编写,在任何出现装箱的地方,编译器会自动地加上执行以上功能的中间代码。
所谓的拆箱就是装箱操作的反操作,把堆中的对象复制到堆栈中,并且返回其值。拆箱操作将判断拆箱的对象类型和将要被复制的值类型引用是否一致,如果不一致将会抛出一个InvalidCastException的异常。这里的类型匹配并不采用任何显式的类型转换。
static void Main(string[] args) { try { Int32 i = 3; Object o = i;//这里装箱 Int16 j = (Int16) o; } catch (Exception e) { Console.WriteLine(e);//将输入出异常 System.InvalidCastException: 指定的转换无效。 } Int32 ii = 3; Object oo = ii;//装箱 Int16 jj = (Int16) (Int32) oo; Console.WriteLine("拆箱成功"); Console.ReadKey(); }
第一组装箱拆箱中,代码试图把一个原来类型为Int32的值类型装箱后的对象拆箱成Int16的变量,这样的拆箱是非法的,会抛出InvalidCastException异常。第二组就OK。
装箱拆箱对性能的影响,如何避免装箱拆箱
装箱拆箱意味着堆和堆栈空间的一系列操作,这些操作的性能代价很大。在堆上的操作相对于堆栈会慢很多,并且可能引发垃圾回收,这些都将大规模的影响系统性能。装箱拆箱操作经常发生在一下两个场合:
值类型的格式化输出
System.Object类型的容器
第一种情况,值类型的格式化输出往往会涉及一次装箱操作。如下代码:
int i = 10; Console.WriteLine("i的值是:"+i);
能够编译通过且执行正确,但引发了一次不必要的装箱。修改如下:
int i = 10; Console.WriteLine("i的值是:" + i.ToString());
i的ToString方法得到的是一个字符串。字符串是引用类型,改动后就不涉及装箱操作。
第二种情况,例如:ArrayList就是一个典型的System.Object容器。以后尽量不要用System.Object容器,可以用泛型来代替,这样可以避免大规模的装箱拆箱。
C#没有传统意义上的全局变量,在C#程序中,任何对象数据都必须属于某个类型。通过公共静态变量,可以实现以前全局变量的所有功能。
类似的方法有很多,例如在ASP.NET应用程序中的Application对象,或者使用配置文件存储可配置数据等。
struct定义的是值类型,class定义的是引用类型,所有的结构对象都分配在堆栈上,所有的类对象都分配在堆上。
在C#中结构可以看做是一个微型类。结构同样支持私有成员变量和公共成员变量,结构支持属性和方法,支持构造函数但有一个小限制,可以重写定义在System.Object中的虚方法。
结构在语法上相对于类的限制:
- struct不支持继承:结构却是继承自System.ValueType,但是结构本身不再支持继承。不能定义一个继承自类或者其他结构的结构,也不能在结构中定义虚方法和抽象方法。
- struct不能自定义无参的构造方法:C#编译器会为结构添加一个无参数的公共构造方法,该方法的作用就是把所有成员变量置0,并且不允许程序员替换。
- struct的成员被自动初始化为0,不能在定义时设置初始值。
struct A { public int a=1;//这里不能通过编译 }
结构常用于存储数据集合,对于涉及复杂操作的类型,应该设计成类而不是结构。
1.类型初始化器的概念
在.NET中,每个类型都会有一个初始化器,程序员可以自己定义初始化器,也可以使用默认的编译器添加的初始化器。类型初始化器和类型有相同的名字,在C#中以static关键字定义,并且没有参数,也没有任何返回值。
public class CCtor { static CCtor() { } }
其中static CCtor() 即是类CCtor的类型初始化器。类型初始化器不同于构造方法,初始化器不能被显示地调用,并且每个类型只能有一个初始化器。CLR保证任何类型的静态成员被使用前,类型初始化器会被调用。这个概念和静态成员变量的初始化表达式非常类似,本质上它们并没有什么区别。
查看如下代码:
namespace MyTest { public class CCtor { public static int m_int = 1;//初始化表达式 static CCtor()//初始化器 { m_int = 2; } public CCtor() { Console.WriteLine("构造方法调用"); } } }
对应的部分IL代码:
可见,C#编译器在编译代码时,会把所有静态成员变量初始化表达式加入到初始化器中,并且添加的位置在自定义的初始化器内容之前。
2.类型初始化器的调用
对与类型初始化器的调用CLR有以下两种策略:
- 在类型的任何成员被使用之前调用类型初始化器,这是类型初始化器的调用时机是可以确定的;
- CLR自由地调用类型初始化器的执行,但保证初始化器在任何一个静态成员变量被调用之前被调用。初始化器保证不会被执行直至有一个静态成员变量被使用,而调用不使用静态成员变量的静态方法或实例方法都不会引发初始化器的执行。
这两种策略受一个元数据特性控制,当一个类型有BeforeFieldInit的元数据特性时,CLR将采用第二种策略,没有则采用第一种策略。C#程序员对这个元数据属性没有添加控制权。对于那些没有显示自定义初始化器的类型,C#编译器将添加BeforeFieldInit元数据属性,而对于那些有自定义初始化器的类型,C#编译器将不添加BeforeFieldInit元数据属性,采用第一种策略。
在性能方面,带有BeforeFieldInit元数据属性的类型,让CLR有更大的调配空间,通常也能得到较好的性能。为了获得更高的效率,程序员首先应该考虑初始化表达式而不是自定义初始化器。
上述结论仅仅是从执行效率来分析的,但作为设计的依据,往往还需要考虑代码的可维护性、健壮性、可扩展性等种种因素。
一共有3个可选关键字可以修饰方法的参数:ref、out、params。
当一个方法参数是引用类型时,传递的本质就是对象的引用。所以ref和out这两个关键字都放生在值类型上。区别是:
- ref关键字要求参数在传入前被初始化
- out关键字不关心参数在传入去是否初始化,但要求参数在返回前被初始化
params关键字允许方法在定义时不确定参数的数量。在params参数之前允许出现不以params关键字修饰的参数,但在其后就不允许出现任何其他参数。
从位置讲:
1.String是.NET Framework里面的,小写的string是C#语言中的
2.如果把using System;删掉,就没有大写的String了,System是.NET Framework类库中的一个函数名。
从性质讲:
1.string是关键字,String是类,string不能作为类、结构、枚举、字段、变量、方法、属性的名称
2.用C编写代码的情况下尽量使用小写的string,比较符合规范,如果在追求效率的情况下可以使用大写的String,因为最终通过编译后,小写的string会变成大写的String,可以给编译减少负荷,从而运行效率提高。
3.string类型表示 Unicode 字符的字符串,string 是 .NET Framework 中的 String 的别名,对字符串相等性的测试更为直观
10 .NET支持哪几种可访问性级别,C#实现了其中的哪几种
.NET中定义了6种可访问性级别,分别是Private、Family、Assembly、Family&Assembly、Family or Assembly、Public。
C#实现了除Family&Assembly之外的5中可访问性级别,对于的关键字是:private、protected、internal、protected internal、public。
C#中属性是指有返回值而无参数的一种特殊的方法,运行程序方便地为成员变量提供一对get/set(或者其中任意一个)方法。属性和Get/Set实现方法本质上没有任何区别。C#编译器在编译时,会把属性转换成一对GetXXX和SetXXX的方法。属性是为了代替暴露的内部成员。由于属性的存在,设计时不再有任何理由把内部成员定义成public。任何需要定义成公开的内部成员都应该设计成private或者protected,并且配上一个属性。这样设计并没有让程序更快的运行,但提高了程序的可扩展性。
public class Person { private string name; public string Name { get { return name; } set { name = value; } } }
对应的IL代码:
浅复制是指复制类型中的所有值类型成员,而只复制应用类型成员的引用,并且使目标对象共享原对象的引用类型成员对象。
深复制是指同时复制值类型和引用类型成员的对象。
浅复制和深复制的概念都是递归的。System.Object中的MemberwiseClone已经实现了浅复制,但它是一个受保护的方法。无论浅复制还是深复制,都可以通过实现ICloneable接口的Clone方法来实现。可被继承的类型需要谨慎地实现ICloneable接口,因为这将导致所有的子类都必须实现ICloneable接口。
C#中有4种循环语法:while、do...while、for和foreach。while和do...while 经常用在循环次数不确定的场合,而for语句和foreach经常用来遍历数组和集合。foreach语句可以遍历任何实现了IEnumerable接口的容器类,但foreach不允许直接或者通过属性修改每个迭代项目的值。
using语句为实现了IDisposable的类型对象调用Dispose方法,using语句能够保证使用的对象的Dispose方法在using语句块结束时被调用,无论是否有异常抛出。C#编译器在编译时会自动为using语句加上try/finally块,所以,using的本质和异常捕获语句一样,但语法更为简洁。所有using使用的对象都应该在using语句开始后再初始化,以保证所有对象能够Dispose。
static void Main(string[] args) { using (StreamReader sr = new StreamReader("1.txt")) { Console.WriteLine(sr.ReadToEnd()); } Console.ReadKey(); }
对应的IL代码:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 41 (0x29) .maxstack 1 .locals init ([0] class [mscorlib]System.IO.StreamReader sr) IL_0000: ldstr "1.txt" IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(string) IL_000a: stloc.0 .try { IL_000b: ldloc.0 IL_000c: callvirt instance string [mscorlib]System.IO.TextReader::ReadToEnd() IL_0011: call void [mscorlib]System.Console::WriteLine(string) IL_0016: leave.s IL_0022 } // end .try finally { IL_0018: ldloc.0 IL_0019: brfalse.s IL_0021 IL_001b: ldloc.0 IL_001c: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0021: endfinally } // end handler IL_0022: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_0027: pop IL_0028: ret } // end of method Program::Main
转载请注明出处: