CSharp
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 | public class ExampleClass { |
const & static
const:
成员常量使用 const
关键字定义,它们必须在声明时初始化,并且初始化的值必须在编译时确定。常量是静态的(即它们属于类本身而不是类的任何实例),尽管 const
关键字本身不包括 static
关键字。常量最常用于定义不会改变的值。
特点:
- 必须在声明时赋值,且赋值必须是编译时常量。
- 在编译时被解析,所以它们不占用运行时内存。
- 不能被修改。
- 访问常量时直接使用类名,无需创建类的实例
static:
成员静态变量(或者静态属性)使用 static
关键字定义,这意味着它们属于类本身,而不是任何特定的类实例。静态变量在所有实例之间共享,因此改变一个实例的静态变量将影响类的所有实例。
注意存在静态构造函数,其专门为静态成员实施构造, 不能访问类中的实例成员
特点:
- 可以在声明时初始化,也可以在构造函数或其他方法中初始化。
- 存储在静态存储区,所有实例共享同一份数据。
- 可以在程序运行期间被修改。
- 同样通过类名访问。
主要区别
- 初始化和修改:常量在编译时初始化且不能修改;静态变量可以在运行时初始化和修改。
- 内存和性能:常量不占用运行时内存,因为它们的值在编译时已确定并嵌入到代码中;而静态变量占用静态存储区的内存。
- 使用场景:常量用于那些在编译时已知且不需要改变的值,如数学中的π;静态变量用于需要跨实例共享的数据,如计数器或配置选项。
is & as 转型
is
检查对象是否兼容指定类型,不抛出异常返回true & false
as
检查对象是否兼容指定类型,返回指定对象或者 null
锁
-
lock
关键字:lock
关键字是最常用的同步机制,它基于Monitor
类实现。当你在代码块前使用lock
,它将阻止多个线程同时执行该代码块。- 示例:
1
2
3
4
5private static readonly object locker = new object();
lock (locker)
{
// 临界区代码
}
-
Monitor
类:Monitor
类提供了一种机制,用于同步对资源的访问。它的功能比lock
更丰富,例如可以在一定时间内尝试获取锁。- 示例:
1
2
3
4
5
6
7
8
9Monitor.Enter(locker);
try
{
// 临界区代码
}
finally
{
Monitor.Exit(locker);
}
-
Mutex
:Mutex
(互斥量)是一个同步原语,可以跨进程工作,用于不同进程之间的同步。- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14using (var mutex = new Mutex(false, "ExampleMutex"))
{
if (mutex.WaitOne())
{
try
{
// 临界区代码
}
finally
{
mutex.ReleaseMutex();
}
}
}
-
Semaphore
和SemaphoreSlim
:- 信号量是一种限制能够同时访问某一资源或资源池的线程数量的机制。
SemaphoreSlim
是一个轻量级的Semaphore
版本,适用于同一进程内的线程同步。- 示例:
1
2
3
4
5
6
7
8
9
10var semaphore = new SemaphoreSlim(initialCount: 3);
semaphore.Wait();
try
{
// 临界区代码
}
finally
{
semaphore.Release();
}
-
ReaderWriterLockSlim
:- 用于管理对资源的读取和写入访问,允许多个读取器或一个写入器。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var 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编译为本机代码,以下是过程:
元数据:一个数据表的集合,描述了模块中定义了什么,引用了什么等一系列的描述表项,VS的智能感知通过解析元数据实现,还能允许对象字段序列化,允许GC跟踪对象生存期。同时由于元数据的存在使得程序集更容易部署