C#学习--进阶篇

C#学习–进阶篇


Q1.为何区分值类型和引用类型

相同点

  • 引用类型可以实现接口,值类型当中的结构体也可以实现接口;
  • 引用类型和值类型都继承自System.Object类。

不同点

  • 引用类型可以派生出新的类型,而值类型不能;
  • 引用类型可以包含null值,值类型不能(可空类型功能允许将 null 赋给值类型);
  • 引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值

内存分配

  • 数组的元素不管是引用类型还是值类型,都存储在托管堆上。
  • 引用类型在栈中存储一个引用,其实际的存储位置位于托管堆。简称引用类型部署在托管推上。而值类型总是分配在它声明的地方:作为字段时,跟随其所属的变量(实 例)存储;作为局部变量时,存储在栈上。(栈的内存是自动释放的,堆内存是.NET中会由GC来自动释放)

适用场景

  • 值类型在内存管理方面具有更好的效率,并且不支持多态,适合用做存储数据的载体;
  • 引用类型支持多态,适合用于定义应用程序的行为。



Q2.为何要装箱和拆箱

定义

  • 装箱是将值类型转换为引用类型 ;拆箱是将引用类型转换为值类型。

为何要装箱

  • 较为典型的场景是:某方法或者容器为了保证通用,将参数定为object,那么在值类型作为参数时就需要装箱

  • 道理很简单,按理说C#被设计成一种完全面向对象的语言。因此,包括数字、字符、日期、布尔值等等在内的一切,都是对象。似乎只需要一种方式来对待这些对象就可以了。

    但是C#不是只停留在学院中和理想中,它必须为性能而妥协,我们知道,对于CPU来说,处理一个完整的对象,需要很多的指令,对于内存来说,又需要很多的内存。如果连整数都是对象,那么性能自然很低。C#于是使用了一种机制,使得这些基本类型在一般的编程中被当作非对象的简单类型处理,在另一些场合,又允许它们被视作是一个对象。这种机制就是装箱和拆箱。

    装箱后的对象看上去和一个对象一样,拥有方法,可以当作object处理,拆箱后的变量,看上去又如同C语言中的那些变量、结构体一样,可以直接参与运算和处理。

    CSDN某帖子四楼


Q3.协变与逆变

定义

查了半天,还是wiki讲的最明白,其他的网站都描述的太抽象了。

  • 协变与逆变是一种术语,描述泛型的一个特性,这个特性具体解释如下:
  • 在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:
    • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
    • 逆变(contravariant),如果它逆转了子类型序关系。
    • 不变(invariant),如果上述两种均不适用。

解释

  • 这样的定义有点抽象,举个栗子来描述一下定义:
  • 如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。
  • 那么我们在这两个类上各构造一个数组,Cat[ ] 与 Animal[ ] (数组是一种泛型吧)
    • 如果Cat[ ] 也是Animal[ ] 的子类,那么数组类型构造器就是 协变,因为通过这个构造器构造的对象保持了其构造类型的父子关系。
    • 如果Animal[ ] 变为了Cat[ ] 的子类,那么该类型构造器就是 逆变,因为通过这个构造器构造的对象逆变了其构造类型的父子关系。
    • 以上两者均不是,则该构造器是 不变 的。
  • 一般的函数类型,对于输入参数是逆变的,对于输出参数是协变的。
    • 例如: 函数类型Cat->Cat可安全用于期望Cat->Animal的地方;类似地,函数类型Animal->Animal可用于期望Cat->Animal的地方——典型地,在 Animal a=Fn(Cat(…)) 这种语境下进行调用,由于 Cat 是 Animal 的子类所以即使 Fn 接受一只 Animal 也同样是安全的。一般规则是:
      S1 → S2 ≦ T1 → T2 当T1 ≦ S1且S2 ≦ T2.
  • 在 C# 中,每个泛型接口的类型参数都可被标注为协变(out)、逆变(in)或不变(不标注)。例如,可以定义一个接口 IEnumerator 作为只读的迭代器,并声明它在其类型参数上具有协变性,代码如下:
1
2
3
4
5
6
7
8
 interface IEnumerator<out T>
{
T Current
{
get;
}
bool MoveNext();
}
  • 通过这样声明,IEnumerator 就会在其类型参数上具有协变性。例如,IEnumerator< Cat > 是 IEnumerator< Animal > 的子类型。
  • 协/逆变合法性:
    • 非泛型类型(类、结构、枚举等)既协变合法、也逆变合法。
    • 类型参数 T 如果没有标 in,那么是协变合法;如果没有标 out,那么是逆变合法。
    • 如果类型 A 是协 / 逆变合法,那么相应的数组类型 A[ ] 是协 / 逆变合法(C# 的数组是协变的)
    • 泛型类型 G<A1, A2, …, An> 是协 / 逆变合法,如果对于每个类型参数 Ai:
      • Ai 是协 / 逆变合法,当且仅当 G 中的第 i 个参数被声明为协变
      • Ai 是逆 / 协变合法(反转),当且仅当 G 中的第 i 个参数被声明为逆变
      • Ai 既协变合法又逆变合法,当且仅当 G 中的第 i 个参数被声明为不变

Q4.C#中造成内存泄露的主要原因

事件(大多数内存泄露的原因)

  • C#的GC是使用标记清除算法来自动回收垃圾的。具体的流程是,从GCRoot开始遍历托管堆内所有对象,将遍历到的对象标记为可达对象,在遍历完成后,回收所有的非可达对象。
    • 注册在事件中的订阅者们由于事件一直存在,所以这些订阅者都一直处于可达状态,注册在事件上的对象一直存在于内存中,造成内存泄露。

非托管对象

  • 非托管对象必须手动释放,必须通过Dispose释放非托管资源。可以使用 using 语句隐式调用Dispose,也可以使用Try/finally语句显式调用Dispose
  • 创建非托管对象一定要注意命名规范,才能避免忘记释放该对象。
  • 使用using语句处理非托管资源示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;
using System.IO;
using System.Text.RegularExpressions;

public class WordCount
{
private String filename = String.Empty;
private int nWords = 0;
private String pattern = @"\b\w+\b";

public WordCount(string filename)
{
if (! File.Exists(filename))
throw new FileNotFoundException("The file does not exist.");

this.filename = filename;
string txt = String.Empty;
using (StreamReader sr = new StreamReader(filename)) {
txt = sr.ReadToEnd();
}
nWords = Regex.Matches(txt, pattern).Count;
}

public string FullName
{ get { return filename; } }

public string Name
{ get { return Path.GetFileName(filename); } }

public int Count
{ get { return nWords; } }
}

集合/静态对象

  • 集合需要及时clear,尤其是将一些方法中的临时变量添加到集合中后,会导致集合膨胀,使其内存泄露。
  • 静态字段在整个程序运行期间都不会释放,所以尽量减少其可见域就减少了其内存泄露的可能性。
  • 尽量不要声明静态的集合,静态的集合很容易造成内存泄露。

Q5.Task 与 Parallel

Updating