C#基础问题

语言&语法本身

基元类型、值类型引用类型

基元类型:最基本的数据类型,通常由编译器直接支持,直接映射到底层硬件的数据处理能力,使得它们在性能上非常高效

值类型:值类型存储在堆栈,值类型在函数调用时传递副本,这意味着在函数中对这些类型的修改不会影响原始数据。

  • 基元类型:所有基元类型也是值类型。
  • 结构体 (struct):例如 .NET 中的 DateTime, TimeSpan 等。
  • 枚举 (enum):例如定义一组命名的常数

值类型:struct、enum、int、float、char、bool、decimal

引用类型:引用类型的变量存储的是数据的引用(即内存地址),而不是数据本身。引用类型的数据存储在托管堆上,生命周期也由垃圾回收器管理

  • (class):如 string, Array 以及用户自定义的任何类。
  • 接口 (interface)
  • 委托 (delegate)
  • 数组:即使数组的元素是值类型,数组本身也是引用类型

引用类型:class、delegate、interface、array、object、string

对于引用类型任何对象它的所有数据成员都存放在堆里,无论是值类型还是引用类型

装箱\拆箱

装箱:将数据项从栈复制到堆,值类型到引用类型转换

(1)第一步:新分配托管堆内存(大小为值类型实例大小加上一个方法表指针。

(2)第二步:将值类型的实例字段拷贝到新分配的内存中。

(3)第三步:返回托管堆中新分配对象的地址。这个地址就是一个指向对象的引用了。

拆箱:(转型)将堆上的数据项提取到栈,引用类型到值类型转换

GC

当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有无被引用的对象,进行清理,并通知栈中的指针重新指向地址排序后的对象。

标记(Mark:找出所有引用不为0(live)的实例) → 计划(Plan:判断是否需要压缩) → 清理(Sweep:回收所有的free空间) → 引用更新(Relocate:将所有引用的地址进行更新) → 压缩(Compact:减少内存碎片)

GC只能处理托管内存资源的释放,对于非托管资源则不能使用GC进行回收,必须由程序员手动回收,例如FileStream或SqlConnection需要调用Dispose进行资源的回收。

值参数&引用参数 vs 值类型&引用类型

  • 值类型(例如 int, struct 等)在内存中直接存储它们的数据。当你将一个值类型的变量赋值给另一个变量时,将复制其内容,因此两个变量完全独立。

  • 引用类型(例如 string, class 实例等)在内存中存储数据的引用(即地址)。当你将一个引用类型的变量赋值给另一个变量时,两个变量将指向相同的内存位置。因此,一个变量的变化会影响到另一个变量。

  • 值参数(Value Parameters):当参数以值的方式传递给函数时,实际传递的是参数的副本。在函数内对这些参数的修改不会影响到原始数据。这是方法参数的默认方式,适用于值类型和引用类型。

  • 引用参数

    (Reference Parameters):通过使用ref或out关键字,可以让函数直接操作原始数据而不是其副本。这意味着函数内的任何改变都会影响到传入的实际对象或变量。

    • 使用 ref 需要在函数调用和声明时都使用 ref 关键字,并且调用前变量需要已被初始化。
    • 使用 out 同样需要在函数调用和声明时使用 out 关键字,不过变量可以不初始化,因为它期望在方法内部被赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExampleClass {
public int Value { get; set; }
}

public void ModifyValue(int number, ref int refNumber, ExampleClass obj, ref ExampleClass refObj) {
number = 10;
refNumber = 20;
obj.Value = 30;
refObj = new ExampleClass { Value = 40 };
}

int a = 1;
int b = 1;
ExampleClass obj1 = new ExampleClass { Value = 5 };
ExampleClass obj2 = new ExampleClass { Value = 5 };

ModifyValue(a, ref b, obj1, ref obj2);

// a 还是 1
// b 变成了 20
// obj1.Value 变成了 30
// obj2 指向了一个新的对象,其 Value 是 40

const & static

const:

成员常量使用 const 关键字定义,它们必须在声明时初始化,并且初始化的值必须在编译时确定。常量是静态的(即它们属于类本身而不是类的任何实例),尽管 const 关键字本身不包括 static 关键字。常量最常用于定义不会改变的值。

特点:

  • 必须在声明时赋值,且赋值必须是编译时常量。
  • 在编译时被解析,所以它们不占用运行时内存。
  • 不能被修改。
  • 访问常量时直接使用类名,无需创建类的实例

static:

成员静态变量(或者静态属性)使用 static 关键字定义,这意味着它们属于类本身,而不是任何特定的类实例。静态变量在所有实例之间共享,因此改变一个实例的静态变量将影响类的所有实例。

注意存在静态构造函数,其专门为静态成员实施构造, 不能访问类中的实例成员

特点:

  • 可以在声明时初始化,也可以在构造函数或其他方法中初始化。
  • 存储在静态存储区,所有实例共享同一份数据。
  • 可以在程序运行期间被修改。
  • 同样通过类名访问。

主要区别

  • 初始化和修改:常量在编译时初始化且不能修改;静态变量可以在运行时初始化和修改。
  • 内存和性能:常量不占用运行时内存,因为它们的值在编译时已确定并嵌入到代码中;而静态变量占用静态存储区的内存。
  • 使用场景:常量用于那些在编译时已知且不需要改变的值,如数学中的π;静态变量用于需要跨实例共享的数据,如计数器或配置选项。

is & as 转型

is 检查对象是否兼容指定类型,不抛出异常返回true & false

as检查对象是否兼容指定类型,返回指定对象或者 null

  1. lock 关键字

    • lock 关键字是最常用的同步机制,它基于 Monitor 类实现。当你在代码块前使用 lock,它将阻止多个线程同时执行该代码块。
    • 示例:
      1
      2
      3
      4
      5
      private static readonly object locker = new object();
      lock (locker)
      {
      // 临界区代码
      }
  2. Monitor

    • Monitor 类提供了一种机制,用于同步对资源的访问。它的功能比 lock 更丰富,例如可以在一定时间内尝试获取锁。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      Monitor.Enter(locker);
      try
      {
      // 临界区代码
      }
      finally
      {
      Monitor.Exit(locker);
      }
  3. Mutex

    • Mutex(互斥量)是一个同步原语,可以跨进程工作,用于不同进程之间的同步。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      using (var mutex = new Mutex(false, "ExampleMutex"))
      {
      if (mutex.WaitOne())
      {
      try
      {
      // 临界区代码
      }
      finally
      {
      mutex.ReleaseMutex();
      }
      }
      }
  4. SemaphoreSemaphoreSlim

    • 信号量是一种限制能够同时访问某一资源或资源池的线程数量的机制。
    • SemaphoreSlim 是一个轻量级的 Semaphore 版本,适用于同一进程内的线程同步。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      var semaphore = new SemaphoreSlim(initialCount: 3);
      semaphore.Wait();
      try
      {
      // 临界区代码
      }
      finally
      {
      semaphore.Release();
      }
  5. ReaderWriterLockSlim

    • 用于管理对资源的读取和写入访问,允许多个读取器或一个写入器。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      var rwLock = new ReaderWriterLockSlim();
      rwLock.EnterReadLock();
      try
      {
      // 读取操作
      }
      finally
      {
      rwLock.ExitReadLock();
      }

      rwLock.EnterWriteLock();
      try
      {
      // 写入操作
      }
      finally
      {
      rwLock.ExitWriteLock();
      }

.NET相关

.NET 框架

软件开发框架,它提供了一个强大的平台,用于构建各种类型的应用程序

.NET框架组成:CLR(公共运行时)、FCL(框架类库包含BCL)、开发工具

CLR:支持多种语言使用的运行时,负责资源管理(包括内存分配、程序及加载、异常处理、线程同步、垃圾回收等),并保证应用和底层操作系统的分离

CLI:一组阐释系统的架构、规则和约定的语言规范

CIL:编译器产生的中间语言由CLI描述含CTS、CLS

.NET优点:更加面向对象的开发环境、GC、互操作性(考虑了不同的.NET语言,win32dll,COM组件)、简化部署

互操作性:允许不同语言间编写的模块无缝交互、允许平台调用(P/Invoke)即允许.NET代码调用非.NET代码(托管与非托管)

简化部署:复制运行(不需要注册表)、并行执行(允许DLL不同版本的存在,每个可执行程序都可以访问程序生成时使用的那个版本的DLL)

.NET 编译相关

经过面向CLR的编译器编译后得到的都是IL(中间语言、托管代码),由CLR管理执行 。此时编译器还得到类型信息、安全信息,当被调用执行时才被JIT编译为本机代码,以下是过程:

源码托管模块(IL与元数据)合并为程序集JIT编译本机代码源码-托管模块(IL与元数据)-合并为程序集-JIT编译-本机代码

元数据:一个数据表的集合,描述了模块中定义了什么,引用了什么等一系列的描述表项,VS的智能感知通过解析元数据实现,还能允许对象字段序列化,允许GC跟踪对象生存期。同时由于元数据的存在使得程序集更容易部署