• 深入理解string和如何高效地使用string

    发布:51Code 时间: 2018-05-14 13:18

  • 前言 个人认为要想把直播从零开始做出来,绝对是牛逼中的牛逼,大牛中的大牛,因为直播中运用到的技术难点非常之多,视频/音频处理,图形处理,视频/音频压缩,CDN分发,即时通...

  • 无论你所使用的是哪种编程语言,我们都不得不承认这样一个共识:string是我们使用最为频繁的一种对象。

    但是string的常用性并不意味着它的简单性,而且我认为,正是由于string的频繁使用才会促使其设计人员在string的设计上花大量的功夫。

    所以正是这种你天天见面的string,蕴含了很多精妙的设计思想。

    一个月以前我写了一篇讨论字符串的驻留(string interning)[1]的文章,我今天将会以字符串的驻留为基础,进一步来讨论.NET中的string。

    string interning的基本前提是string的恒定性(immutability),即string一旦被创建将不会改变。

    我们就先来谈谈string的恒定性。

    一 string是恒定的(immutable)

    和其他类型比较,string最为显著的一个特点就是它具有恒定不变性:我们一旦创建了一个string,在managed heap上为他分配了一块连续的内存空间,我们将不能以任何方式对这个string进行修改使之变长、变短、改变格式。

    所有对这个string进行各项操作(比如调用ToUpper获得大写格式的string)而返回的string,实际上另一个重新创建的string,其本身并不会产生任何变化。

    String的恒定性具有很多的好处,它首先保证了对于一个既定string的任意操作不会造成对其的改变,同时还意味着我们不用考虑操作string时候出现的线程同步的问题。

    在string恒定的这些好处之中,我觉得最大的好处是:它成就了字符串的驻留。

    CLR通过一个内部的interning table保证了CLR只维护具有不同字符序列的string,任何具有相同字符序列的string所引用的均为同一个string对象,同一段为该string配分的内存块。

    字符串的驻留极大地较低了程序执行对内存的占用。

    对于string的恒定性和字符串的驻留,还有一点需要特别指出的是:string的恒定性不单单是针对某一个单独的AppDomain,而是针对一个进程的。

    二 String可以跨AppDomain共享的(cross-appDomain)

    我们知道,在一个托管的环境下,Appdomain是托管程序运行的一个基本单元。AppDomain为托管程序提供了良好的隔离机制,保证在同一个进程中的不同的Appdomain不可以共享相同的内存空间。

    在一个Appdomain创建的对象不能被另一个Appdomain直接使用,对象在AppDomain之间传递需要有一个Marshaling的过程:对象需要通过by reference或者by value的方式从一个Appdomain传递到另一个Appdomain。

    具体内容可以参照我的另一篇文章:用Coding证明Appdomain的隔离性[2]。

    但是这里有一个特例,那就是string。

    Appdomain的隔离机制是为了防止一个Application的对内存空间的操作对另一个Application 内存空间的破坏。

    通过前面的介绍,我们已经知道了string是恒定不变的、是只读的。所以它根本不需要Appdomain的隔离机制。

    所以让一个恒定的、只读的string被同处于一个进程的各个Application共享是没有任何问题的。

    String的这种跨AppDomain的恒定性成就了基于进程的字符串驻留:一个进程中各个Application使用的具有相同字符序列的string都是对同一段内存的引用。我们将在下面通过一个Sample来证明这一点。

    三 证明string垮AppDomain的恒定性

    在写这篇文章的时候,我对如何证明string跨AppDomain的interning,想了好几天,直到我偶然地想到了为实现线程同步的lock机制。

    我们知道在一个多线程的环境下,为了避免并发操作导致的数据的不一致性,我们需要对一个对象加锁来阻止该对象被另一个线程操作。

    相反地,为了证明两个对象是否引用的同一个对象,我们只需要在两个线程中分别对他们加锁,如果程序执行的效果和对同一个对象加锁的情况完全一样的话,那么就可以证明这两个被加锁的对象是同一个对象。

    基于这样的原理我们来看看我们的Sample:

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.Threading;

     

    namespace Artech.ImmutableString

    {

     class Program

     {

      static void Main(string[] args)

      { 

       var appDomain1 = AppDomain

        .CreateDomain("Artech.AppDomain1");

       var appDomain2 = AppDomain

        .CreateDomain("Artech.AppDomain2");

       var marshalByRefObj1 = appDomain1

        .CreateInstanceAndUnwrap("Artech.ImmutableString", 

         "Artech.ImmutableString.MarshalByRefType")

          as MarshalByRefType;

       var marshalByRefObj2 = appDomain2

        .CreateInstanceAndUnwrap("Artech.ImmutableString", 

         "Artech.ImmutableString.MarshalByRefType") 

          as MarshalByRefType;

       marshalByRefObj1.StringLockHelper = "Hello World";

       marshalByRefObj2.StringLockHelper = "Hello World";

       var thread1 = new Thread(

        new ParameterizedThreadStart(Execute));

       var thread2 = new Thread(

        new ParameterizedThreadStart(Execute));

       thread1.Start(marshalByRefObj1);

       thread2.Start(marshalByRefObj2);

       Console.Read();            

      }

     

      static void Execute(object obj)

      { 

       var marshalByRefObj = obj as MarshalByRefType;

       marshalByRefObj.ExecuteWithStringLocked();

      }

     }

     

     class MarshalByRefType : MarshalByRefObject

     {

      #region Private Fields

      private string m_StringLockHelper;

      private object m_ObjectLockHelper;

      #endregion

     

      #region Public Properties

      public string StringLockHelper

      {

       get { return m_StringLockHelper; }

       set { m_StringLockHelper = value; }

      }

     

      public object ObjectLockHelper

      {

       get { return m_ObjectLockHelper; }

       set { m_ObjectLockHelper = value; }

      }

      #endregion

     

      #region Public Methods

      public void ExecuteWithStringLocked()

      {

       lock (m_StringLockHelper)

       {

        Console.WriteLine("The operation with a string locked is executed\n\tAppDomain:\t{0}\n\tTime:\t\t{1}",

         AppDomain.CurrentDomain.FriendlyName, 

         DateTime.Now);

        Thread.Sleep(10000);

       }

      }

     

      public void ExecuteWithObjectLocked()

      {

       lock (m_ObjectLockHelper)

       {

        Console.WriteLine("The operation with a object locked is executed\n\tAppDomain:\t{0}\n\tTime:\t\t{1}",

         AppDomain.CurrentDomain.FriendlyName, 

         DateTime.Now);

        Thread.Sleep(10000);

       }

      }

      #endregion

     }    

    }

    我们来简单地分析一下上面的coding。

    我们创建了一个继承自MarshalByRefObject的Class,因为我需要让它具有跨AppDomain传递的能力。

    在这个Class中定义了两个为实现线程同步的helper字段,一个是string类型的m_StringLockHelper,另一个是object类型的m_ObjectLockHelper,并为他们定义了相应的Property。

    此外定义了两个方法:ExecuteWithStringLocked和ExecuteWithStringLocked,他们的操作类似:在先对m_StringLockHelper和m_ObjectLockHelper加锁的前提下,输出操作执行的AppDomain和确切时间。我们通过调用Thread.Sleep模拟10s的时间延迟。

    在Main方法中,首先创建了两个AppDomain,名称分别为Artech.AppDomain1和Artech.AppDomain2。

    随后在这两个AppDomain中创建两个MarshalByRefType对象,并为它们的StringLockHelper属性赋上相同的值:Hello World。

    最后,我们创建了两个新的线程,并在它们中分别调用在两个不同AppDomain 中创建的MarshalByRefType对象的ExecuteWithStringLocked方法。

    我们来看看运行后的输出结果:

    从上面的输出结果中可以看出,两个分别在不同线程中执行操作对应的AppDomain的name分别为Artech.AppDomain1和Artech.AppDomain2。

    执行的时间(确切地说是操作成功地对MarshalByRefType对象的m_StringLockHelper字段进行加锁的时间)相隔10s,也就是我们在程序中定义的时间延迟。

    为什么会出现这样的结果呢?我们只是对两个处于不同AppDomain的不同MarshalByRefType对象的m_StringLockHelper字段进行加锁。由于我们是同时开始他们对应的线程,照理说它们之间不会有什么关联,显示出来的时间应该是相同的。

    唯一的解释就是:虽然这两个在不同的AppDomain中创建的对象是两个完全不同的对象,由于他们的m_StringLockHelper字段具有相同的字符序列,它们引用的是同一个string。这就证明了我们提出的跨AppDomain进行string interning的结论。

    为了进一步印证我们的结论,我们使两个MarshalByRefObject对象的m_StringLockHelper字段具有不同的值,看看结果又如何。于是我们把其中一个对象的m_StringLockHelper字段改为”Hello World!”(多加了一个!) 。

    marshalByRefObj1.StringLockHelper = "Hello World";

    marshalByRefObj2.StringLockHelper = "Hello World!";

    看看现在的输出结果,现在的时间是一样了。

    上面我们做的是对string类型字段加锁的试验。那么我们对其他类型的对象进行加锁,又会出现怎么的情况呢?

    我们现在就来做这样试验:在各自的线程中调用两个对象的ExecuteWithObjectLocked方法。

    我们修改Execute方法和Main()。

    static void Execute(object obj)

     var marshalByRefObj = obj as MarshalByRefType;

     marshalByRefObj. ExecuteWithObjectLocked ();

    }

     

    static void Main(string[] args)

    {

     var appDomain1 = AppDomain

      .CreateDomain("Artech.AppDomain1");

     var appDomain2 = AppDomain

      .CreateDomain("Artech.AppDomain2");

     var marshalByRefObj1 = appDomain1

      .CreateInstanceAndUnwrap("Artech.ImmutableString", 

       "Artech.ImmutableString.MarshalByRefType") as MarshalByRefType;

     var marshalByRefObj2 = appDomain2

      .CreateInstanceAndUnwrap("Artech.ImmutableString", 

       "Artech.ImmutableString.MarshalByRefType") as MarshalByRefType;

     var obj = new object();

     marshalByRefObj1.ObjectLockHelper = obj;

     marshalByRefObj2.ObjectLockHelper = obj;

     var thread1 = new Thread(new ParameterizedThreadStart(Execute));

     var thread2 = new Thread(new ParameterizedThreadStart(Execute));

     thread1.Start(marshalByRefObj1);

     thread2.Start(marshalByRefObj2);

     Console.Read();

    }

    我们先来看看运行后的输出结果:

    我们发现两个时间是一样的,那么就是说两个对象的ObjectLockHelper引用的不是同一个对象。虽然上面的程序很简单,我觉得里面涉及的规程却很值得一说。

    我们来分析下面3行代码。

    object obj = new object();

    marshalByRefObj1.ObjectLockHelper = obj;

    marshalByRefObj2.ObjectLockHelper = obj;

    简单看起来,两个MarshalByRefObject对象的ObjectLockHelper都是引用的同一个对象obj。

    但是背后的情况没有那么简单。代码第一行创建了一个新的对象obj,这个对象是在当前AppDomain 中创建的。而对于当前的AppDomain来说,marshalByRefObj1和marshalByRefObj2仅仅是一个Transparent proxy而已,它们包含一个在Artech.AppDomain1和Artech.AppDomain2中创立的MarshalByRefObject对象的引用。

    我们为它的ObjectLockHelper属性赋值,对于Transparent proxy对象的赋值调用会传到真正对象所在的AppDomain,由于obj是当前AppDomain的对象,它不能直接赋给另一个AppDomain的对象。所以它必须经历一个Marshaling的过程才能被传递到另外一个AppDomain。

    实际上述赋值操作完成之后,真正的ObjectLockHelper属性对应的对象是根据原数据重建的对象,和在当前AppDomain中的对象已经没有任何的关系。所以两个MarshalByRefObject对象的ObjectLockHelper属性引用的并不是同一个对象,所以对它进行加锁对彼此不会产生任何影响。

    四 从Garbage Collection的角度来看string

    我们知道在一个托管的环境下,一个对象的生命周期被GC管理和控制。一个对象只有在他不被引用的时候,GC才会对他进行垃圾回收。

    而对于一个string来说,它始终被interning table引用,而这个interning table是针对一个Process的,是被该Process所有AppDomain共享的,所以一个string的生命周期相对比较长,只有所有的AppDomain都不具有对该string的引用时,他才有可能被垃圾回收。

    五 从多线程的角度来看string

    一方面由于string的恒定性,我们不用考虑多线程的并发操作产生的线程同步问题。

    另一方面由于字符串的驻留,我们在对一个string对象进行加锁操作的时候,极有可能拖慢这个Application的performance,就像我们的Sample中演示的那样。而且很有可能影响到处于同一进程的其他Application,以致造成死锁。

    所以我们在使用锁的时候,除非万不得已,切忌对一个string进行加锁。

    六 如何高效地使用string

    下面简单介绍一些高效地使用string的一些小的建议:

    1. 尽量使用字符串(literal string)相加来代替字符串变量和字符串相加,因为这样可以使用现有的string操作指令进行操作和利用字符串驻留。

    比如:

    string s = "abc" + "def";

    优于

    string s = "abc";

    s = s + "def";

    2. 在需要的时候使用StringBuilder对string作频繁的操作。

    由于string的恒定性,在我们对一个string进行某些操作的时候,比如调用ToUpper()或者ToLower()把某个string每个字符转化成大写或者小写;调用SubString()取子串;会创建一个新的string,有时候会创建一些新的临时string。这样的操作会增加内存的压力。

    所有在对string作频繁操作的情况下,我们会考虑使用StringBuilder来高效地操作string。StringBuilder之所以能对string操作带来更好的performance,是因为在它的内部维护一个字符数组而不是一个string,来避免string操作带来的新的string的创建。

    StringBuilder是一个很好的字符累加器,我们应该充分地利用这一个功能:

    var sb = new StringBuilder();

    sb.Append(str1 + str2);

    最好写成

    var sb = new StringBuilder();

    sb.Append(str1);

    sb.Append(str2);

    避免创建一个新的临时string来保存str1 + str2。

    再比如下面的Code

    var sb = new StringBuilder();

    sb.Append(WorkOnString1());

    sb.Append(WorkOnString2());

    sb.Append(WorkOnString3());

    最好把WorkOnString1,WorkOnString2,WorkOnString3定义成:

    WorkOnString1(StringBuilder sb)

    WorkOnString2(StringBuilder sb)

    WorkOnString3(StringBuilder sb)

    3. 高效地进行string的比较操作

    我们知道,对象之间的比较有比较Value和比较Reference之说。一般地对Reference进行比较的速度最快。

    对于string,在字符串驻留的前提下,我们可以把对Value的比较用Reference的比较来代替从而使Performance提升。

    此外,对于忽略大小写的比较,我们最好使用string的static方法Compare(string strA, string strB, bool ignoreCase)。

    也就是说:

    if(str1.ToLower() == str2.ToLower())

    最好写成:

    If(string. Compare(str1, str2, true))

    文章来源:网络
    上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8103),我们将立即处理。

  • 上一篇:从C#到TypeScript - 类型

    下一篇:JavaWeb与Asp.net工作原理比较分析

网站导航
Copyright(C)51Code软件开发网 2003-2018 , 沪ICP备16012939号-1