【.NET】| 总结/Edison Zhou
此系列文章为我在2015年发布于博客园的.NET基础拾遗系列,它十分适合初中级.NET开发工程师在面试前进行一个系统的复习,因此我将其搬到公众号分享与你。
本文为第一篇,我们会对.NET的基础类型和语法进行基础复习,全文会以Q/A的形式展现,即以面试题的形式来描述。
1.NET中的所有类型的基类是什么?
在.NET中所有的内建类型都继承自System.Object类型。在C#中,不需要显示地定义类型继承自System.Object,编译器将自动地自动地为类型添加上这个继承申明,以下两行代码的作用完全一致:
public class A
{
}
public class A : System.Object
{
}
2值类型和引用类型的区别是什么?
在.NET中的类型分为值类型和引用类型,它们各有特点,其共同点是都继承自System.Object,但最明显的区分标准却是是否继承自System.ValueType(System.ValueType继承自System.Object),也就是说所有继承自System.ValueType的类型是值类型,而其他类型都是引用类型。常用的值类型包括:结构、枚举、整数型、浮点型、布尔型等等;而在C#中所有以class关键字定义的类型都是引用类型。
严格来讲,System.Object作为所有内建类型的基类,本身并没有值类型和引用类型之分。但是System.Object的对象,具有引用类型的特点。这也是值类型在某些场合需要装箱和拆箱操作的原因。
(1)赋值时的区别
这是值类型与引用类型最显著的一个区别:值类型的变量直接将获得一个真实的数据副本,而对引用类型的赋值仅仅是把对象的引用赋给变量,这样就可能导致多个变量引用到一个对象实例上。
(2)内存分配的区别
引用类型的对象将会在堆上分配内存,而值类型的对象则会在堆栈上分配内存。堆栈空间相对有限,但是运行效率却比堆高很多。
(3)继承结构的区别
由于所有的值类型都有一个共同的基类System.ValueType,因此值类型具有了一些引用类型所不具有的共同性质,比较重要的一点就是值类型的比较方法:Equals。所有的值类型已经实现了内容的比较(而不再是引用地址的比较),而引用类型没有重写Equals方法还是采用引用比较。
3装箱与拆箱的原理是什么?
(1)装箱
CLR需要做额外的工作把堆栈上的值类型移动到堆上,这个操作就被称为装箱。
(2)拆箱
装箱操作的反操作,把堆中的对象复制到堆栈中,并且返回其值。
装箱和拆箱都意味着堆和堆栈空间的一系列操作,毫无疑问,这些操作的性能代价是很大的,尤其对于堆上空间的操作,速度相对于堆栈的操作慢得多,并且可能引发垃圾回收,这些都将大规模地影响系统的性能。因此,我们应该避免任何没有必要的装箱和拆箱操作。
如何避免呢?
首先分析装箱和拆箱经常发生的场合:
① 值类型的格式化输出
② System.Object类型的容器
对于第①种情况,我们可以通过下面的改动示例来避免:
int i = 10;
Console.WriteLine("The value is {0}", i.ToString());
对于第②种情况,则可以使用泛型技术来避免使用针对System.Object类型的容器,有效避免大规模地使用装箱和拆箱:
ArrayList arrList = new ArrayList();
arrList.Add(0);
arrList.Add("1");
// 使用泛型数据结构代替ArrayList
List<int> intList = new List<int>();
intList.Add(1);
intList.Add(2);
4struct与class的区别是什么?
首先,struct(结构)是值类型,而class(类)是引用类型,所有的结构对象都分配在堆栈上,而所有的类对象都分配在堆上。
其次,struct与class相比,不具备继承的特性,struct虽然可以重写定义在System.Object中的虚方法,但不能定义新的虚方法和抽象方法。
最后,struct不能有无参数的构造方法(class默认就有),也不能为成员变量定义初始值。
public struct A
{
public int a = 1; // 这里不能编译通过
}
结构对象在构造时必须被初始化为0,构造一个全0的对象是指在内存中为对象分配一个合适的空间,并且把该控件置为0。
问题来了:如何使用struct or class?
答:当一个类型仅仅是原始数据的集合,而不需要复杂的操作时,就应该设计为struct,否则就应该设计为一个class。
5C#中方法的参数传递有几种方式?
(1)ref关键字:引用传递参数,需要在传递前初始化;(ref 要求参数在传入前被初始化)
(2)out关键字:引用传递参数,需要在返回前初始化;(out 要求参数在方法返回前被初始化)
Note:ref和out这两个关键字的功能极其类似,都用来说明该参数以引用方式进行传递。大家都知道,.NET的类型分为引用类型和值类型,当一个方法参数是引用类型时,传递的本质就是对象的引用。所以,这两个关键字的作用都发生在值类型上。
(3)params关键字:允许方法在定义时不确定参数的数量,这种形式非常类似数组参数,但形式更加简洁易懂。
But,params关键字的使用也有一定局限:当一个方法申明了一个params参数后,就不允许在其后面再有任何其他参数。
例如下面一段代码,定义了两个完全相等的方法:NotParams和UseParams,使用由params修饰参数的方法时,可以直接把所有变量集合传入而无须先申明一个数组对象。
class Program
{
static void Main(string[] args)
{
// params
string s = "I am a string";
int i = 10;
double f = 2.3;
object[] par = new object[3] { s, i, f };
// not use params
NotParams(par);
// use params
UseParams(s, i, f);
Console.ReadKey();
}
// Not use params
public static void NotParams(object[] par)
{
foreach (var obj in par)
{
Console.WriteLine(obj);
}
}
// Use params
public static void UseParams(params object[] par)
{
foreach (var obj in par)
{
Console.WriteLine(obj);
}
}
}
6浅复制与深复制的区别是什么?
(1)浅复制
复制一个对象的时候,仅仅复制原始对象中所有的非静态类型成员和所有的引用类型成员的引用。(新对象和原对象将共享所有引用类型成员的实际对象)
(2)深复制
复制一个对象的时候,不仅复制所有非静态类型成员,还要复制所有引用类型成员的实际对象。
下图展示了浅复制和深复制的区别:
在.NET中,基类System.Object已经为所有类型都实现了浅复制,类型所要做的就是公开一个复制的接口,而通常的,这个接口会由ICloneable接口来实现。ICloneable只包含一个方法Clone,该方法既可以被实现为浅复制也可以被实现为深复制,具体如何取舍则根据具体类型的需求决定。
此外,在System.Object基类中,有一个保护的MemeberwiseClone()方法,它便用于进行浅度复制。所以,对于引用类型,要想实现浅度复制时,只需要调用这个方法就可以了:
public object Clone()
{
return MemberwiseClone();
}
下面的代码展示了一个使用ICloneable接口提供深复制的简单示例:
public class DeepCopy : ICloneable
{
public int i = 0;
public A a = new A();
public object Clone()
{
// 实现深复制-方式1:依次赋值和实例化
DeepCopy newObj = new DeepCopy();
newObj.a = new A();
newObj.a.message = this.a.message;
newObj.i = this.i;
return newObj;
}
public new object MemberwiseClone()
{
// 实现浅复制
return base.MemberwiseClone();
}
public override string ToString()
{
string result = string.Format("I的值为{0},A为{1}", this.i.ToString(), this.a.message);
return result;
}
}
public class A
{
public string message = "我是原始A";
}
public class Program
{
static void Main(string[] args)
{
DeepCopy dc = new DeepCopy();
dc.i = 10;
dc.a = new A();
DeepCopy deepClone = dc.Clone() as DeepCopy;
DeepCopy shadowClone = dc.MemberwiseClone() as DeepCopy;
// 深复制的目标对象将拥有自己的引用类型成员对象
deepClone.a.message = "我是深复制的A";
Console.WriteLine(dc);
Console.WriteLine(deepClone);
Console.WriteLine();
// 浅复制的目标对象将和原始对象共享引用类型成员对象
shadowClone.a.message = "我是浅复制的A";
Console.WriteLine(dc);
Console.WriteLine(shadowClone);
Console.ReadKey();
}
}
其执行结果如下图所示,可以清楚地看到对深复制对象的属性的赋值不会影响原始对象,而浅复制则相反。
从上面的代码中可以看到,在深复制的实现中,如果每个对象都要这样去进行深度复制就太麻烦了,我们可以利用序列化/反序列化来对对象进行深度复制,即先把对象序列化(Serialize)到内存中,然后再进行反序列化,通过这种方式来进行对象的深度复制:
[Serializable]
public class DeepCopy : ICloneable
{
......
public object Clone()
{
// 实现深复制-方式1:依次赋值和实例化
//DeepCopy newObj = new DeepCopy();
//newObj.a = new A();
//newObj.a.message = this.a.message;
//newObj.i = this.i;
//return newObj;
// 实现深复制-方式2:序列化/反序列化
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, this);
ms.Position = 0;
return bf.Deserialize(ms);
}
......
}
[Serializable]
public class A
{
public string message = "我是原始A";
}
Note:一般可被继承的类型应该避免实现ICloneable接口,因为这样做将强制所有的子类型都需要实现ICloneable接口,否则将使类型的深复制不能覆盖子类的新成员。
End总结
本文总结复习了.NET的基础类型和语法相关的重要知识点,下一篇会总结.NET内存管理相关的重要知识点,欢迎继续关注!
参考资料(全是经典)
朱毅,《进入IT企业必读的200个.NET面试题》
张子阳,《.NET之美:.NET关键技术深入解析》
王涛,《你必须知道的.NET》
年终总结:Edison的2020年终总结
数字化转型:我在传统企业做数字化转型
C#刷题:C#刷剑指Offer算法题系列文章目录
商业知识:IT技术人的底层商业知识兵器库
.NET大会:2020年中国.NET开发者大会PDF资料
👇扫码关注EdisonTalk