跳转至

01.基础语法

.NET/C#面试题汇总系列:基础语法

1.字符串中 string str=null 和 string str=""和 string str=string.Empty 的区别?

string.Empty 相当于"",Empty 是⼀个静态只读的字段。

string str="" ,初始化对象,并分配⼀个空字符串的内存空间。

string str=null,初始化对象,不会分配内存空间。

2.byte b = 'a'; byte c = 1; byte d = 'ab'; byte e = '啊'; byte g = 256; 这些变量有哪些错误?是错在哪⾥?

本题考查的是数据类型能承载数据的⼤⼩。

1byte =8bit,1 个汉字=2 个 byte,1 个英⽂=1 个 byte=8bit。

所以 bc 是对的,deg 是错的。

'a'是 char 类型,a 错误。字符 a 的 ascii 码为 97,在 byte 范围内的值,所以可以修改为 byte b = (byte)'a'。

java byte 取值范围是 -128~127, ⽽ C#⾥⼀个 byte 是 0~255。

3.string 和 StringBuilder 的区别,两者性能的⽐较。

  • 不可变性:

string 是不可变的。一旦创建了一个字符串对象,就不能更改其内容。对字符串进行修改实际上是创建一个新的字符串对象。

StringBuilder 是可变的。它允许对字符串进行动态的、原地的修改,而不必每次都创建新的对象。

  • 性能比较:

当需要频繁对字符串进行修改时,StringBuilder 通常比直接使用 string 更高效。这是因为 StringBuilder 允许原地修改,而不必每次都创建新的字符串对象,从而减少了内存开销。

对于简单的字符串拼接,如果只涉及少量操作,性能差异可能并不明显。但在大量拼接或修改的情况下,StringBuilder 的性能优势会更为明显。

  • 内存分配:

都是引⽤类型,分配在托管堆上。

在使用 string 进行字符串拼接时,每次拼接都会创建一个新的字符串对象,这可能导致频繁的内存分配和垃圾回收。

StringBuilder 的内部实现使用可变的字符数组,避免了频繁的内存分配,从而提高了性能。

  • 线程安全性:

string 是不可变的,因此是线程安全的。多个线程可以同时读取一个字符串对象而不会引起问题。

StringBuilder 不是线程安全的。如果多个线程同时尝试修改同一个 StringBuilder 实例,可能会引发竞态条件。如果在多线程环境中需要对字符串进行修改,应该考虑使用 StringBuilder 的同步方法或采取其他线程安全的措施。

综上所述,选择使用 string 还是 StringBuilder 取决于具体的使用场景。如果字符串是不变的且不需要频繁修改,使用 string 是合适的。如果有⼤量⽆法预知次数的字符串操作进行字符串的拼接或修改,尤其是在循环中,使用 StringBuilder 可以提高性能。

扩充

StringBuilder 记忆点:

  • 默认容量 16,长度 0
  • 缓冲区就是一个字符数组 char []

查看 StringBuilder 源码


《CLR via C#》Ch 14.3

由于 String 类型代表不可变字符串,所以 FCL 提供了 System.Text.StringBuilder 类型对字符串和字符进行高效动态处理,并返回处理好的 String 对象。可将 StringBuilder 想像成创建 String 对象的特殊构造器。你的方法一般应获取 String 参数而非 StringBuilder 参数。

从逻辑上说,StringBuilder 对象包含一个字段,该字段引用了由 Char 结构构成的数组。可利用 StringBuilder 的各个成员来操纵该字符数组,高效率地缩短字符串或更改字符串中的字符。如果字符串变大,超过了事先分配的字符数组大小,StringBuilder 会自动分配个新的、更大的数组,复制字符,并开始使用新数组。前一个数组被垃圾回收。

用 StringBuilder 对象构造好字符串后,调用 StringBuilder 的 ToString 方法即可将 StringBuilder 的字符数组“转换”成 String。这样会在堆上新建 String 对象,其中包含调用 ToString 时存在于 StringBuilder 中的字符串。之后可继续处理 StringBuilder 中的字符串。以后可再次调用 ToString 把它转换成另一个 String 对象。

4.什么是扩展⽅法?

扩展方法(Extension Methods)是 C#中一种特殊的静态方法,它允许你向现有的类添加新的方法,而无需修改原始类的定义。扩展方法通常用于向.NET 框架中的类型添加功能,甚至是无法修改的封闭源代码的类。⼀句话解释,扩展⽅法使你能够向现有类型“添加”⽅法,⽆需修改类型。

条件:按扩展⽅法必须满⾜的条件,1.必须要静态类中的静态⽅法 2.第⼀个参数的类型是要扩展的类型,并且需要添加 this 关键字以标识其为扩展⽅法。

建议:通常,只在不得已的情况下才实现扩展⽅法,并谨慎的实现。

使⽤:不能通过类名调⽤,直接使⽤类型来调⽤。

5.byte a =255;a+=5;a 的值是多少?

byte 定义:表示 8 位无符号整数,取值范围是 \([0,{2^8}-1]\),即 0 到 255,a+=1 时,a 的值时 0,所 以 a+=5 时,值是 4。

数据溢出处理机制:数值在到达极大值后,继续累加,会变成极小值。

注意下图代码产生“错误”的时机:前者只会造成事实错误,没有任何异常抛出;后者存在编译错误,IDE 在编码阶段已给出提示。

Snipaste_2024-12-12_17-07-16

扩充

(来源:《C#语言参考》

常见类型取值范围一览

整形数值类型

C# 类型/关键字 范围 大小 .NET 类型
sbyte -128 到 127 8 位带符号整数 System.SByte
byte 0 到 255 无符号的 8 位整数 System.Byte
short -32,768 到 32,767 有符号 16 位整数 System.Int16
ushort 0 到 65,535 无符号 16 位整数 System.UInt16
int -2,147,483,648 到 2,147,483,647 带符号的 32 位整数 System.Int32
uint 0 到 4,294,967,295 无符号的 32 位整数 System.UInt32
long -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 64 位带符号整数 System.Int64
ulong 0 到 18,446,744,073,709,551,615 无符号 64 位整数 System.UInt64
nint 取决于(在运行时计算的)平台 带符号的 32 位或 64 位整数 System.IntPtr
nuint 取决于(在运行时计算的)平台 无符号的 32 位或 64 位整数 System.UIntPtr

浮点数值类型

C# 类型/关键字 大致范围 精度 大小 .NET 类型
float ±1.5 x 10−45 至 ±3.4 x 1038 大约 6-9 位数字 4 个字节 System.Single
double ±5.0 × 10−324 到 ±1.7 × 10308 大约 15-17 位数字 8 个字节 System.Double
decimal ±1.0 x 10-28 至 ±7.9228 x 1028 28-29 位 16 个字节 System.Decimal

布尔类型(bool):取值 true 或 false。

字符类型(char):取值范围是 Unicode UTF-16 字符集的所有字符。

类型 范围 大小 .NET 类型
char U+0000 到 U+FFFF 16 位 System.Char

枚举类型(enum):取决于枚举定义时的基础类型。

引用类型:不包含实际值,而是指向对象的引用。

6.什么是装箱和拆箱?

装箱就是隐式地将⼀个值类型转换成引⽤类型,如:

int i=0;
Syste.Object obj=i;

拆箱就是将引⽤类型转换成值类型,如:

int i=0;
System.Object obj=i;
int j=(int)obj;//将obj拆箱

7.值类型和引⽤类型的区别?

值类型变量是直接包含值。将⼀个值类型变量赋给另⼀个值类型变量,是复制包含的值,默认值是 0。

引⽤类型变量的赋值只复制对对象的引⽤,⽽不复制对象本身,默认值是 null。

值类型有整形、浮点型、bool、枚举。

引⽤类型有 class、delegate、object、string。

值类型存储在栈中,引⽤类型存储在堆中。

8.new 关键字的作⽤?

运算符:创建对象实例。

修饰符:在派⽣类定义⼀个重名的⽅法,隐藏掉基类⽅法。

约束:泛型约束定义,约束可使⽤的泛型类型,如:

public class ItemFactory<T> where T : IComparable, new()
{
}

9. int?和 int 有什么区别?

int?为可空类型,默认值可以是 null,int 默认值是 0。 int?是通过 int 装箱为引⽤类型实现。

10. C#中的委托是什么?

⼀句话解释就是:将⽅法当作参数传⼊另⼀个⽅法的参数。 .net 中有很多常⻅的委托如:Func、Action。 作⽤:提⾼⽅法的扩展性。

11.⽤最有效的⽅法算出 2 乘以 8 等于⼏?

位运算是最快,使⽤的是位运算 逻辑左位移<<。

⽅法是 2<<3 相当于 0000 0000 0000 0010(2 的 16 位 int ⼆进制),左移三位就是 0000 0000 0001 0000(16 的⼆进制)。

扩充

(来源:《C#语言参考》

位运算符和移位运算符

位运算符和移位运算符包括一元位补、二进制左移和右移、无符号右移、二进制逻辑 AND、OR 和异或运算符。这些操作数采用整型数值类型字符型操作数。

12.const 和 readonly 有什么区别?

都可以标识⼀个常量。主要有以下区别:

1、初始化位置不同。const 必须在声明的同时赋值;readonly 即可以在声明处赋值,也可以在静态构造⽅法(必须是静态构造⽅法,普通构造⽅法不⾏)⾥赋值。 2、修饰对象不同。const 即可以修饰类的字段,也可以修饰局部变量;readonly 只能修饰类的字段 3、const 是编译时常量,在编译时确定该值;readonly 是运⾏时常量,在运⾏时确定该值。 4、const 默认是静态的;⽽ readonly 如果设置成静态需要显示声明 5、修饰引⽤类型时不同,const 只能修饰 string 或值为 null 的其他引⽤类型;readonly 可以是任何类型。

13.现有⼀个整数 number,请写⼀个⽅法判断这个整数是否是 2 的 N 次⽅

数据示例:4(100)、5(101)、8(1000)、16(10000)

取模运算:⽤ number%2==0 只能是 2 的整数倍,无法判断是 2 的 N 次方。

位运算:当一个整数是 2 的 N 次方时,它的二进制表示只有一位是 1,其余位都是 0。例如:2 的 3 次方是 8,其二进制表示为 00001000;2 的 4 次方是 16,其二进制表示为 00010000。如果将一个 2 的 N 次方的整数减去 1,会得到一个所有低位都是 1 的数。例如:对于 2 的 3 次方的整数 8,减去 1 得到 7,其二进制表示为 00000111;对于 2 的 4 次方的整数 16,减去 1 得到 15,其二进制表示为 00001111。

当我们对这两个数进行按位与操作 (number & (number - 1)),如果结果等于 0,则表示只有一位是 1,也就是指定的整数是 2 的 N 次方。

代码实现:

// 判断 number 是否是 2 的 N 次方
private static bool IsPowerOfTwo(int number)
{
    // 判断是否为负数或零 && 使用位运算判断是否只有一位是 1
    return (number > 0) && ((number & (number - 1)) == 0);
}

// 已知 number 是 2 的 N 次方,求 N 的值
private static int GetN1(int number)
{
    int n = 1;
    while (number / 2 != 1)
    {
        number = number / 2;
        //number >>= 1;//除法可用右移运算符代替
        n += 1;
    }
    return n;
}

14.CTS、CLS、CLR 分别作何解释

CTS:通⽤语⾔系统。CLS:通⽤语⾔规范。CLR:公共语⾔运⾏库。

CTS:Common Type System 通⽤类型系统。Int32、Int16→int、String→string、Boolean→bool。 每种语⾔都定义了⾃⼰的类型,.Net 通过 CTS 提供了公共的类型,然后翻译⽣成对应的.Net 类型。

CLS:Common Language Specification 通⽤语⾔规范。不同语⾔语法的不同。每种语⾔都有⾃⼰的语 法,.Net 通过 CLS 提供了公共的语法,然后不同语⾔翻译⽣成对应的.Net 语法。

CLR:Common Language Runtime 公共语⾔运⾏时,就是 GC、JIT 等这些。有不同的 CLR,⽐如服务 器 CLR、Linux CLR(Mono)、Silverlight CLR(CoreCLR)。相当于⼀个发动机,负责执⾏ IL。

15.在.net 中,配件的意思是?

程序集。(中间语⾔,源数据,资源,装配清单)

16.分析下⾯代码,a、b 的值是多少?

string strTmp = "a1 某某某";
int a1 = System.Text.Encoding.Default.GetBytes(strTmp).Length;
int b2 = strTmp.Length;

分析:⼀个字⺟、数字占⼀个 byte,⼀个中⽂占占两个 byte,所以 a=8,b=5

17.String s = new String(“xyz”);创建了⼏个 String Object?

题目错误,C#的 String 不存在参数为 string 类型的构造器。

Snipaste_2024-12-12_16-39-16


如果 Java 的题目,则回答如下:

String s = new String("xyz");

分两种情况

  • 如果 String 常量池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象 new String(“xyz”),此时为一个 Obeject 对象;
  • 如果 String 常量池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象 new String(“xyz”),此时为二个 Obeject 对象;

String str="xyz"; 毫无疑问,这行代码创建了1 个 String 对象

String a="abc"; String b="abc"; 那这里呢? 答案还是1 个(String 常量池中,已经创建"abc",则不会继续创建)。

String a="ab"+"cd"; 再看看这里呢? 答案是3 个

扩充

string 记忆点

CLR 字符串留用机制


《CLR via C#》Ch 14.2.4

如果应用程序经常对字符串进行区分大小写的序号比较,或者事先知道许多字符串对象都有相同的值,就可利用 CLR 的字符串留用 (stringinterming) 机制来显著提升性能。CLR 初始化时会创建一个内部哈希表。在这个表中,键 (key) 是字符串,而值 (value) 是对托管堆中的 String 对象的引用。哈希表最开始是空的 (理应如此)。String 类提供了两个方法,便于你访问这个内部哈希表:

public static string Intern(string str);

public static String IsInterned(string str);

第一个方法 Intern 获取一个 String,获得它的哈希码,并在内部哈希表中检查是否有相匹配的。如果存在完全相同的字符串,就返回对现有 String 对象的引用。如果不存在完全相同的字符串,就创建字符串的副本,将副本添加到内部哈希表中,返回对该副本的引用。如果应用程序不再保持对原始 String 对象的引用,垃圾回收器就可释放那个字符串的内存注意垃圾回收器不能释放内部哈希表引用的字符串,因为哈希表正在容纳对它们的引用。除非卸载 AppDomain 或进程终止,否则内部哈希表引用的 String 对象不能被释放。 和 Intern 方法一样,IsInterned 方法也获取一个 String,并在内部哈希表中查找它。如果哈希表中有匹配的字符串,IsInterned 就返回对这个留用 (IsInterned) 字符串对象的引用。但如果没有,IsInterned 会返回 null,不会将字符串添加到哈希表中。

18.静态成员和⾮静态成员的区别

1.静态成员⽤ static 修饰符声明,在类被实例化时创建,通过类进⾏访问

2.不带 static 的变量时⾮静态变量,在对象被实例化时创建,通过对象进⾏访问,

3.静态⽅法⾥不能使⽤⾮静态成员,⾮静态⽅法可以使⽤静态成员

4.静态成员属于类,⽽不属于对象

19.C#可否对内存直接操作

C#在 unsafe 模式下可以使⽤指针对内存进⾏操作,但在托管模式下不可以使⽤指针,C#.NET 默认不运⾏ 带指针的,需要设置下 VS,选择项⽬右键->属性->选择⽣成->“允许不安全代码”打勾->保存。

/// <summary>
/// unsafe 方法示例
/// </summary>
static unsafe void Main()
{
    int a = 1;

    // 使用 unsafe 关键字和指针来直接修改内存
    int* p = &a;
    *p = 9;
    Console.WriteLine(a); // 输出:9
}

20.short s1 = 1; s1 = s1 + 1;有什么错?short s1 = 1; s1 += 1;有什么错?

前者存在编译错误,s1+1 不能显式转换成 short 类型,可以修改为 s1 =(short)(s1 + 1) ;short s1 = 1; s1 += 1 正确

21. 什么是强类型,什么是弱类型?哪种更好些?为什么?

强类型是在编译的时候就确定类型的数据,在执⾏时类型不能更改,⽽弱类型在执⾏的时候才会确定类 型。没有好不好,⼆者各有好处,强类型安全,因为它事先已经确定好了,⽽且效率⾼。

⼀般⽤于编译型编程语⾔,如 c++、java、c#、pascal 等,弱类型相⽐⽽⾔不安全,在运⾏的时候容易出现错 误,但它灵活,多⽤于解释型编程语⾔,如 python、javascript、vb、php 等。

22.using 关键字的作⽤

1.引⽤命名空间。

2.using 还可以用于为类型或命名空间创建别名,以解决命名冲突或简化类型名的使用。

3.自动释放资源。

实现了 IDisposiable 的类在 using 中创建,using 结束后会⾃定调⽤该对象的 Dispose ⽅法, 释放资源。

23. ref 和 out 有什么区别

ref 和 out 都是在 C#中用于参数传递的关键字,它们有一些区别,主要体现在以下几个方面:

初始值:

  • ref 关键字要求在传递给方法之前必须先为变量赋初值,即在方法调用前必须对变量进行初始化。
  • out 关键字不要求在传递给方法之前为变量赋初值,但在方法内部必须确保在使用该参数之前赋值。
// ref 的示例
int x = 1;
MyMethod(ref x); // 必须在调用前为 x 赋值
// out 的示例
int y;
MyMethod(out y); // 不需要在调用前为 y 赋值

方法内部对参数的要求:

  • ref 关键字在方法内部不要求对变量重新赋值,但可以在方法内对其进行修改。

  • out 关键字在方法内部要求对变量重新赋值,因为方法内部不能使用未赋值的 out 参数。

// ref 的示例
void Method(ref int a)
{
  // 可以修改 a 的值
  a = a + 1;
}
// out 的示例
void Method(out int b)
{
  // 必须在方法内部为 b 赋值
  b = 998;
}

方法调用时的要求:在方法调用时,ref 和 out 关键字都要求在实参和形参上都使用相同的修饰符。

// ref 的示例
int x = 1;
Method(ref x);
// out 的示例
int y;
Method(out y);

总的来说,ref 和 out 都允许将参数的引用传递给方法,但它们在对初始值和在方法内部的要求上有所不同。选择使用哪个关键字取决于具体的需求和设计。通常,如果方法需要从参数中获取值并可能对其进行修改,可以使用 ref;如果方法只需要返回值,并且不关心参数的初始值,可以使用 out。

24.a.Equals(b) 和 a==b ⼀样吗?

不一样。

Equals 方法是从 System.Object 类继承而来的,因此对于所有类型都是可用的。默认情况下,Equals 方法执行的是引用比较,即检查两个对象是否引用同一个内存位置。子类可以重写 Equals 方法以提供自定义的相等性比较。

== 操作符的行为取决于具体的类型。对于引用类型,== 执行的是引用比较,与 Object.ReferenceEquals 方法的行为相同。 对于值类型,== 操作符通常执行值比较,即比较两个对象的值是否相等。但某些值类型可以通过重写 == 操作符来改变这种行为。

25.下⾯这段代码求值

class Class1
{
    internal static int count = 0;
    static Class1()
    {
        count++;
    }
    public Class1()
    {
        count++;
    }
}
Class1 o1 = new Class1();
Class1 o2 = new Class1();

o1.count=2

o2.count=3

静态构造⽅法计算⼀次,两个实例化对象计算两次。

26.关于构造函数说法正确的是哪个?

a) 构造函数可以声明返回类型。

b) 构造函数不可以⽤ private 修饰

c) 构造函数必须与类名相同

d) 构造函数不能带参数

答案:c,构造函数必须与类名相同,可以传递多个传递,作⽤就是便于初始化对象成员,不能有任何返 回类型。

27.Math.Round(11.5) 等于多少?Math.Round(-11.5) 等于多少?

在 C#中,Math.Round 方法用于将浮点数舍入到最接近的整数。对于包含 .5 的情况,它遵循一种特定的规则,称为"银行家舍入"规则。所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。

Math.Round(11.5) = 12

Math.Round(-11.5) = -12

28.&和&&的区别

& 是按位与运算符,返回结果是 int 类型。同时也可用于逻辑与操作,但不会短路。

&& 是逻辑与运算符,返回结果是 bool 类型。具有短路的特性。

29. i++ 和 ++i 有什么区别?

i++ 和 ++i 都是用于递增变量的操作符,它们的区别在于它们返回的值和执行顺序。

i++(后增量):

i++ 表示使用变量的当前值,然后再将变量递增。 返回的值是变量的当前值,然后变量会递增。 后增量表示先使用当前值再递增。

int i = 5;
int result = i++; // result 的值是 5,i 的值变为 6

++i(前增量):

++i 表示先将变量递增,然后再使用递增后的值。 返回的值是递增后的变量值。 前增量表示先递增再使用。

int i = 5;
int result = ++i; // result 的值是 6,i 的值也变为 6

在实际使用中,这两种形式的选择通常取决于具体的需求。如果你希望先使用当前值再递增,可以使用后增量(i++)。如果你希望先递增再使用递增后的值,可以使用前增量(++i)。

30. as 和 is 的区别

as 运算符用于将对象转换为指定类型,如果转换失败则返回 null,而不会引发异常。

is 运算符用于检查对象是否是指定类型的实例,返回值类型为布尔型。