.NET面试题解析(03)-string与字符串操作

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 转自:  常见面试题目:1.字符串是引用类型类型还是值类型?2.在字符串连接处理中,最好采用什么方式,理由是什么?3.使用 StringBuilder时,需要注意些什么问题?4.以下代码执行后内存中会存在多少个字符串?分别是什么?输出结果是什么?为什么呢?string st1 = "123" + "abc";string st2 = "123abc";Console.

转自:  

常见面试题目:

1.字符串是引用类型类型还是值类型?

2.在字符串连接处理中,最好采用什么方式,理由是什么?

3.使用 StringBuilder时,需要注意些什么问题?

4.以下代码执行后内存中会存在多少个字符串?分别是什么?输出结果是什么?为什么呢?

string st1 = "123" + "abc";
string st2 = "123abc";
Console.WriteLine(st1 == st2);
Console.WriteLine(System.Object.ReferenceEquals(st1, st2));

5.以下代码执行后内存中会存在多少个字符串?分别是什么?输出结果是什么?为什么呢?

string s1 = "123";
string s2 = s1 + "abc";
string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

6.使用C#实现字符串反转算法,例如:输入"12345", 输出"54321"。

7.下面的代码输出结果?为什么?

复制代码
object a = "123";
object b = "123";
Console.WriteLine(System.Object.Equals(a,b));
Console.WriteLine(System.Object.ReferenceEquals(a,b));
string sa = "123";
Console.WriteLine(System.Object.Equals(a, sa));
Console.WriteLine(System.Object.ReferenceEquals(a, sa));
复制代码

  深入浅出字符串操作

string是一个特殊的引用类型,使用上有点像值类型。之所以特殊,也主要是因为string太常用了,为了提高性能及开发方便,对string做了特殊处理,给予了一些专用特性。为了弥补string在字符串连接操作上的一些性能不足,便有了StringBuilder。

大笑 认识string

首先需要明确的,string是一个引用类型,其对象值存储在托管堆中。string的内部是一个char集合,他的长度Length就是字符char数组的字符个数。string不允许使用new string()的方式创建实例,而是另一种更简单的语法,直接赋值(string aa= “000”这一点也类似值类型)。

认识string,先从一个简单的示例代码入手:

public void DoStringTest()
{
    var aa = "000";
    SetStringValue(aa);
    Console.WriteLine(aa);
}

private void SetStringValue(string aa)
{
    aa += "111";
}

上面的输出结果为“000”。

通过前面的值类型与引用类型的文章,我们知道string是一个引用类型,既然是一个引用类型,参数传递的是引用地址,那为什么不是输出“000111”呢?是不是很有值类型的特点呢!这一切的原因源于string类型的两个重要的特性:恒定性与驻留性

书呆子 String的恒定性(不变性)

字符串是不可变的,字符串一经创建,就不会改变,任何改变都会产生新的字符串。比如下面的代码,堆上先创建了字符串s1=”a”,加上一个字符串“b”后,堆上会存在三个个字符串实例,如下图所示。

string s1 = "a";
string s2 = s1 + "b";

image

上文中的”任何改变都会产生新的字符串“,包括字符串的一些操作函数,如str1.ToLower,Trim(),Remove(int startIndex, int count),ToUpper()等,都会产生新的字符串,因此在很多编程实践中,对于字符串忽略大小的比较:

if(str1.ToLower()==str2.ToLower()) //这种方式会产生新的字符串,不推荐
if(string. Compare(str1,str2,true)) //这种方式性能更好

恼怒 String的驻留性

由于字符串的不变性,在大量使用字符串操作时,会导致创建大量的字符串对象,带来极大的性能损失。因此CLR又给string提供另外一个法宝,就是字符串驻留,先看看下面的代码,字符串s1、s2竟然是同一个对象!

var s1 = "123";
var s2 = "123";
Console.WriteLine(System.Object.Equals(s1, s2));  //输出 True
Console.WriteLine(System.Object.ReferenceEquals(s1, s2));  //输出 True

相同的字符串在内存(堆)中只分配一次,第二次申请字符串时,发现已经有该字符串是,直接返回已有字符串的地址,这就是驻留的基本过程。

字符串驻留的基本原理:

  • CLR初始化时会在内存中创建一个驻留池,内部其实是一个哈希表,存储被驻留的字符串和其内存地址。
  • 驻留池是进程级别的,多个AppDomain共享。同时她不受GC控制,生命周期随进程,意思就是不会被GC回收(不回收!难道不会造成内存爆炸吗?不要急,且看下文)
  • 当分配字符串时,首先会到驻留池中查找,如找到,则返回已有相同字符串的地址,不会创建新字符串对象。如果没有找到,则创建新的字符串,并把字符串添加到驻留池中。

如果大量的字符串都驻留到内存里,而得不到释放,不是很容易造成内存爆炸吗,当然不会了?因为不是任何字符串都会驻留,只有通过IL指令ldstr创建的字符串才会留用。

字符串创建的有多种方式,如下面的代码:

var s1 = "123";
var s2 = s1 + "abc";
var s3 = string.Concat(s1, s2);
var s4 = 123.ToString();
var s5 = s2.ToUpper();

其IL代码如下

image

在上面的代码中,出现两个字符串常量,“123”和“abc”,这个两个常量字符串在IL代码中都是通过IL指令ldstr创建的,只有该指令创建的字符串才会被驻留,其他方式产生新的字符串都不会被驻留,也就不会共享字符串了,会被GC正常回收。

那该如何来验证字符串是否驻留呢,string类提供两个静态方法:

  • String.Intern(string str) 可以主动驻留一个字符串;
  • String.IsInterned(string str);检测指定字符串是否驻留,如果驻留则返回字符串,否则返回NULL

image

请看下面的示例代码

var s1 = "123";
var s2 = s1 + "abc";
Console.WriteLine(s2);   //输出:123abc
Console.WriteLine(string.IsInterned(s2) ?? "NULL");   //输出:NULL。因为“123abc”没有驻留

string.Intern(s2);   //主动驻留字符串
Console.WriteLine(string.IsInterned(s2) ?? "NULL");   //输出:123abc

 

眨眼 认识StringBuilder

大量的编程实践和意见中,都说大量字符串连接操作,应该使用StringBuilder。相对于string的不可变,StringBuilder代表可变字符串,不会像字符串,在托管堆上频繁分配新对象,StringBuilder是个好同志。

首先StringBuilder内部同string一样,有一个char[]字符数组,负责维护字符串内容。因此,与char数组相关,就有两个很重要的属性:

  • public int Capacity:StringBuilder的容量,其实就是字符数组的长度。
  • public int Length:StringBuilder中实际字符的长度,>=0,<=容量Capacity。

StringBuilder之所以比string效率高,主要原因就是不会创建大量的新对象,StringBuilder在以下两种情况下会分配新对象:

  • 追加字符串时,当字符总长度超过了当前设置的容量Capacity,这个时候,会重新创建一个更大的字符数组,此时会涉及到分配新对象。
  • 调用StringBuilder.ToString(),创建新的字符串。

追加字符串的过程:

  • StringBuilder的默认初始容量为16;
  • 使用stringBuilder.Append()追加一个字符串时,当字符数大于16,StringBuilder会自动申请一个更大的字符数组,一般是倍增;
  • 在新的字符数组分配完成后,将原字符数组中的字符复制到新字符数组中,原字符数组就被无情的抛弃了(会被GC回收);
  • 最后把需要追加的字符串追加到新字符数组中;

简单来说,当StringBuilder的容量Capacity发生变化时,就会引起托管对象申请、内存复制等操作,带来不好的性能影响,因此设置合适的初始容量是非常必要的,尽量减少内存申请和对象创建。代码简单来验证一下:

StringBuilder sb1 = new StringBuilder();
Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=16; Length=0;   //初始容量为16 
sb1.Append('a', 12);    //追加12个字符
Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=16; Length=12;  
sb1.Append('a', 20);    //继续追加20个字符,容量倍增了
Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=32; Length=32;  
sb1.Append('a', 41);    //追加41个字符,新容量=32+41=73
Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //输出:Capacity=73; Length=73;  

StringBuilder sb2 = new StringBuilder(80); //设置一个合适的初始容量
Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=0;
sb2.Append('a', 12);
Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=12;
sb2.Append('a', 20);
Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=32;
sb2.Append('a', 41);
Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //输出:Capacity=80; Length=73;

为什么少量字符串不推荐使用StringBuilder呢?因为StringBuilder本身是有一定的开销的,少量字符串就不推荐使用了,使用String.Concat和String.Join更合适。

 

吐舌笑脸 高效的使用字符串

  • 在使用线程锁的时候,不要锁定一个字符串对象,因为字符串的驻留性,可能会引发不可以预料的问题;
  • 理解字符串的不变性,尽量避免产生额外字符串,如:
if(str1.ToLower()==str2.ToLower()) //这种方式会产生新的字符串,不推荐
if(string. Compare(str1,str2,true)) //这种方式性能更好
  • 在处理大量字符串连接的时候,尽量使用StringBuilder,在使用StringBuilder时,尽量设置一个合适的长度初始值;
  • 少量字符串连接建议使用String.Concat和String.Join代替。

  题目答案解析:

1.字符串是引用类型类型还是值类型?

引用类型。

2.在字符串连加处理中,最好采用什么方式,理由是什么?

少量字符串连接,使用String.Concat,大量字符串使用StringBuilder,因为StringBuilder的性能更好,如果string的话会创建大量字符串对象。

3.使用 StringBuilder时,需要注意些什么问题?

  • 少量字符串时,尽量不要用,StringBuilder本身是有一定性能开销的;
  • 大量字符串连接使用StringBuilder时,应该设置一个合适的容量;

4.以下代码执行后内存中会存在多少个字符串?分别是什么?输出结果是什么?为什么呢?

string st1 = "123" + "abc";
string st2 = "123abc";
Console.WriteLine(st1 == st2);
Console.WriteLine(System.Object.ReferenceEquals(st1, st2));

输出结果:

True
True

内存中的字符串只有一个“123abc”,第一行代码(string st1 = "123" + "abc"; )常量字符串相加会被编译器优化。由于字符串驻留机制,两个变量st1、st2都指向同一个对象。IL代码如下:

image

5.以下代码执行后内存中会存在多少个字符串?分别是什么?输出结果是什么?为什么呢?

string s1 = "123";
string s2 = s1 + "abc";
string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

和第5题的结果肯定是不一样的,答案留给读者吧,文章太长了,写的好累!

6.使用C#实现字符串反转算法,例如:输入"12345", 输出"54321"

这是一道比较综合的考察字符串操作的题目,答案可以有很多种。通过不同的答题可以看出程序猿的基础水平。下面是网上比较认可的两种答案,效率上都是比较不错的。

public static string Reverse(string str)
{
    if (string.IsNullOrEmpty(str))
    {
        throw new ArgumentException("参数不合法");
    }

    StringBuilder sb = new StringBuilder(str.Length);  //注意:设置合适的初始长度,可以显著提高效率(避免了多次内存申请)
    for (int index = str.Length - 1; index >= 0; index--)
    {
        sb.Append(str[index]);
    }
    return sb.ToString();
}
public static string Reverse(string str)
{
    if (string.IsNullOrEmpty(str))
    {
        throw new ArgumentException("参数不合法");
    }
    char[] chars = str.ToCharArray();
    int begin = 0;
    int end = chars.Length - 1;
    char tempChar;
    while (begin < end)
    {
        tempChar = chars[begin];
        chars[begin] = chars[end];
        chars[end] = tempChar;
        begin++;
        end--;
    }
    string strResult = new string(chars);
    return strResult;
}

还有一个比较简单也挺有效的方法:

public static string Reverse(string str)
{
    char[] arr = str.ToCharArray();
    Array.Reverse(arr);
    return new string(arr);
}

7.下面的代码输出结果?为什么?

复制代码
object a = "123";
object b = "123";
Console.WriteLine(System.Object.Equals(a,b));
Console.WriteLine(System.Object.ReferenceEquals(a,b));
string sa = "123";
Console.WriteLine(System.Object.Equals(a, sa));
Console.WriteLine(System.Object.ReferenceEquals(a, sa));
复制代码

输出结果全是True,因为他们都指向同一个字符串实例,使用object声明和string声明在这里并没有区别(string是引用类型)。

使用object声明和string声明到底有没有区别呢?,有点疑惑,一个朋友在面试时面试官有问过这个问题,那个面试官说sa、a是有区别的,且不相等。对于此疑问,欢迎交流。

相关文章
|
9天前
|
存储 算法 安全
.NET 平台 SM2 国密算法 License 证书生成深度解析
授权证书文件的后缀通常取决于其编码格式和具体用途。本文档通过一个示例程序展示了如何在 .NET 平台上使用国密 SM2 算法生成和验证许可证(License)文件。该示例不仅详细演示了 SM2 国密算法的实际应用场景,还提供了关于如何高效处理大规模许可证文件生成任务的技术参考。通过对不同并发策略的性能测试,开发者可以更好地理解如何优化许可证生成流程,以满足高并发和大数据量的需求。 希望这段描述更清晰地传达了程序的功能和技术亮点。
64 13
.NET 平台 SM2 国密算法 License 证书生成深度解析
|
4月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
141 2
|
5月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
289 3
|
3月前
|
Java 程序员
面试官的加分题:super关键字全解析,轻松应对!
小米,29岁程序员,通过一个关于Animal和Dog类的故事,详细解析了Java中super关键字的多种用法,包括调用父类构造方法、访问父类成员变量及调用父类方法,帮助读者更好地理解和应用super,应对面试挑战。
55 3
|
4月前
|
存储 安全 Java
美团面试:String 为什么 不可变 ?(90%答错了,尼恩来一个绝世答案)
45岁老架构师尼恩分享Java面试心得,涵盖String不可变性、字符串常量池、面试技巧等内容。尼恩强调,掌握深层技术原理,如String不可变性的真正原因,可在面试中脱颖而出,赢得高薪Offer。此外,尼恩还提供了大量技术资源和面试指导,帮助求职者提升技术水平,顺利通过大厂面试。
|
4月前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
271 2
|
4月前
|
存储 NoSQL MongoDB
MongoDB面试专题33道解析
大家好,我是 V 哥。今天为大家整理了 MongoDB 面试题,涵盖 NoSQL 数据库基础、MongoDB 的核心概念、集群与分片、备份恢复、性能优化等内容。这些题目和解答不仅适合面试准备,也是日常工作中深入理解 MongoDB 的宝贵资料。希望对大家有所帮助!
107 7
|
4月前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
95 1
|
5月前
|
测试技术 API 开发者
精通.NET单元测试:MSTest、xUnit、NUnit全面解析
【10月更文挑战第15天】本文介绍了.NET生态系统中最流行的三种单元测试框架:MSTest、xUnit和NUnit。通过示例代码展示了每种框架的基本用法和特点,帮助开发者根据项目需求和个人偏好选择合适的测试工具。
133 3
|
6月前
|
缓存 Android开发 开发者
Android RecycleView 深度解析与面试题梳理
本文详细介绍了Android开发中高效且功能强大的`RecyclerView`,包括其架构概览、工作流程及滑动优化机制,并解析了常见的面试题。通过理解`RecyclerView`的核心组件及其优化技巧,帮助开发者提升应用性能并应对技术面试。
161 8

热门文章

最新文章

推荐镜像

更多