.NET基础拾遗(3)字符串、集合和流2

二、常用集合和泛型

2.1 int[]是值类型还是引用类型?

  .NET中无论是存储值类型对象的数组还是存储引用类型的数组,其本身都是引用类型,其内存也都是分配在堆上的。所有的数组类型都继承自System.Array,而System.Array又实现了多个接口且直接继承自System.Object。、

不同之处则在于存储值类型对象的数组所有的值都已经包含在数组内,而存储引用类型对象的数组,其值则是一个引用,指向位于托管堆中的实例对象。

  下图直观地展示了二者内存分配的差别(假设object[]中存储都是DateTime类型的对象实例):

  在.NET中CLR会检测所有对数组的访问,任何视图访问数组边界以外的代码都会产生一个IndexOutOfRangeException异常。

2.2 数组之间如何进行转换?

  数组类型的转换需要遵循以下两个原则:

  (1)包含值类型的数组不能被隐式转换成其他任何类型

  (2)两个数组类型能够相互转换的一个前提是两者维数相同

  我们可以通过以下代码来看看数组类型转换的机制:

    // 编译成功
    string[] sz = { "a", "a", "a" };
    object[] oz = sz;
    // 编译失败,值类型的数组不能被转换
    int[] sz2 = { 1, 2, 3 };
    object[] oz2 = sz;
    // 编译失败,两者维数不同
    string[,] sz3 = { { "a", "b" }, { "a", "c" } };
    object[] oz3 = sz3;

  除了类型上的转换,我们平时还可能会遇到内容转换的需求。例如,在一系列的用户界面操作之后,系统的后台可能会得到一个DateTime的数组,而现在的任务则是将它们存储到数据库中,而数据库访问层提供的接口只接受String[]参数,这时我们要做的就是把DateTime[]从内容上转换为String[]对象。当然,惯常做法是遍历整个源数组,逐一地转换每个对象并且将其放入一个目标数组类型容器中,最后再生成目标数组。But,这里我们推荐使用Array.ConvertAll方法,它提供了一个简便的转换数组间内容的接口,我们只需指定源数组的类型、对象数组的类型和具体的转换算法,该方法就能高效地完成转换工作。

  下面的代码清楚地展示了普通的数组内容转换方式和使用Array.ConvertAll的数组内容转换方式的区别:

    class Program
    {
        static void Main(string[] args)
        {
            String[] times ={"2008-1-1",
                            "2008-1-2",
                            "2008-1-3"};

            // 使用不同的方法转换
            DateTime[] result1 = OneByOne(times);
            DateTime[] result2 = ConvertAll(times);

            // 结果是相同的
            Console.WriteLine("手动逐个转换的方法:");
            foreach (DateTime item in result1)
            {
                Console.WriteLine(item.ToString("yyyy-MM-dd"));
            }
            Console.WriteLine("使用Array.Convert方法:");
            foreach (DateTime item2 in result2)
            {
                Console.WriteLine(item2.ToString("yyyy-MM-dd"));
            }

            Console.ReadKey();
        }

        // 逐个手动转换
        private static DateTime[] OneByOne(String[] times)
        {
            List<DateTime> result = new List<DateTime>();
            foreach (String item in times)
            {
                result.Add(DateTime.Parse(item));
            }
            return result.ToArray();
        }

        // 使用Array.ConertAll方法
        private static DateTime[] ConvertAll(String[] times)
        {
            return Array.ConvertAll(times,
                new Converter<String, DateTime>
                (DateTimeToString));
        }

        private static DateTime DateTimeToString(String time)
        {
            return DateTime.Parse(time);
        }
    }
View Code

  从上述代码可以看出,二者实现了相同的功能,但是Array.ConvertAll不需要我们手动地遍历数组,也不需要生成一个临时的容器对象,更突出的优势是它可以接受一个动态的算法作为具体的转换逻辑。当然,明眼人一看就知道,它是以一个委托的形式作为参数传入,这样的机制保证了Array.ConvertAll具有较高的灵活性。

2.3 简述泛型

  泛型是.NET 2.0中推出的众多特性中最为重要的一个,方便我们设计更加通用的类型,也避免了容器操作中的装箱和拆箱操作带来的性能损失

  假如要实现一个排序算法,要能够针对各种类型进行排序。按以前的做法需要对int、double、float等类型都实现一次,但是发现除了数据类型,其他的处理逻辑完全一致。这时便可以考虑使用泛型来进行实现:

    public static class SortHelper<T> where T : IComparable
    {
        public static void BubbleSort(T[] array)
        {
            int length = array.Length;
            for (int i = 0; i <= length - 2; i++)
            {
                for (int j = length - 1; j >= 1; j--)
                {
                    // 对两个元素进行交换            
                    if (array[j].CompareTo(array[j - 1]) < 0)
                    {
                        T temp = array[j];
                        array[j] = array[j - 1];
                        array[j - 1] = temp;
                    }
                }
            }
        }
    }

Microsoft在产品文档中建议所有的泛型参数名称都以T开头,作为编码的通用规范,建议大家都能遵守这样的规范。

  通常泛型类被称为开放式类型,.NET中规定开放式类型不能实例化,也就确保了开放式类型的泛型参数在被指定前,不会被实例化成任何对象。为开放式的类型提供泛型的实例导致了一个封闭类型的生成,但这并不代表新的封闭类型和开放类型有任何继承关系,它们在类结构图上是处于同一层次且两者之间没有任何关系。

  此外,在.NET中的System.Collections.Generic命名空间下提供了诸如List<T>、Dictionary<T>、LinkedList<T>等泛型数据结构,并且在System.Array中定义了一些静态的泛型方法,我们应该在编码实践时充分使用这些泛型容器,以提高我们的开发和系统的运行效率

2.4 泛型的主要约束和次要约束是什么?

  一个泛型参数没有任何约束时可以进行的操作和运算是非常有限的,因为不能对实参进行任何类型上的保证。

     C#编译器在编译的过程中可以根据约束来检查所有泛型类型的实参并确保其满足约束条件。

  (1)主要约束

  一个泛型参数至多拥有一个主要约束,主要约束可以是一个引用类型、class或者struct。如果指定一个引用类型(class),那么实参必须是该类型或者该类型的派生类型。相反,struct则规定了实参必须是一个值类型。下面的代码展示了泛型参数主要约束:

    public class ClassT1<T> where T : Exception
    {
        private T myException;
        public ClassT1(T t)
        {
            myException = t;
        }
        public override string ToString()
        {
            // 主要约束保证了myException拥有source成员
            return myException.Source;
        }
    }

    public class ClassT2<T> where T : class
    {
        private T myT;
        public void Clear()
        {
            // T是引用类型,可以置null
            myT = null;
        }
    }

    public class ClassT3<T> where T : struct
    {
        private T myT;
        public override string ToString()
        {
            // T是值类型,不会发生NullReferenceException异常
            return myT.ToString();
        }
    }
View Code

  泛型参数有了主要约束后,也就能够在类型中对其进行一定的操作了。

  (2)次要约束

  次要约束主要是指实参实现的接口的限定。对于一个泛型,可以有0到无限的次要约束,次要约束规定了实参必须实现所有的次要约束中规定的接口。次要约束与主要约束的语法基本一致,区别仅在于提供的不是一个引用类型而是一个或多个接口。例如我们为上面代码中的ClassT3增加一个次要约束:

    public class ClassT3<T> where T : struct, IComparable
    {
        ......      
    }
View Code

作一点补充:   

 事实上,泛型对性能的提升仅仅对值类型有效,对引用类型几乎可以忽略不计,因为引用类型不需要装箱拆箱(不严谨)。 引用类型的泛型可以共用代码,而对于值类型来说,JIT会为每种值类型生成各自的封闭类型,并不会节省代码量,事实上,还会加重JIT编译的负担——尽管几乎可以忽略不计。   个人认为,泛型真正的优势在于编译时类型安全,结合IDE的智能提示,极大地提高了编码效率,至于性能,试想下,我们有多少情况会用到纯粹值类型的泛型呢?大部分情况下都还是引用类型的泛型。   甚至在某些极端情况下,泛型有略微降低性能的情况——对于引用类型来说。但这些依然不能妨碍其成为.NET平台最有意义的一次升级之一。

原文地址:https://www.cnblogs.com/tiantianle/p/5616203.html