鸟语天空
数组(System.Array)
post by:追风剑情 2021-3-8 9:38

本章内容:

初始化数组元素
● 数组转型
● 所有数组都隐式派生自 System.Array
● 所有数组都隐式实现IEnumerable,ICollection 和 IList
● 数组的传递和返回
● 创建下限非零的数组
● 数组的内部工作原理
● 不安全的数组访问和固定大小的数组

数组是允许将多个数据项作为集合来处理的机制。CLR 支持一维、多维和交错数组(即数组)构成的数组)。所有数组类型都隐式地从 System.Array 抽象类派生,后者又派生自System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚地说 明了这一点:
Int32[] myIntegers; // 声明一个数组引用
myIntegers = new Int32[100];// 创建含有 100 个 Int32 的数组
第一行代码声明 myIntegers 变量,它能指向包含 Int32 值的一维数组。myIntegers 刚开始被设为 null,因为当时还没有分配数组。第二行代码分配了含有 100 个 Int32 值的数组,所有 Int32 都被初始化为 0。由于数组是引用类型,所以会在托管堆上分配容纳 100 个未装箱 Int32 所需的内存块。实际上,除了数组元素,数组对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员(这是额外的成员称为overhead字段或者说“开销字段”)。该数组的内存块地址被返回并保存到myIntegers 变量中。

多维数组

// 创建一个三维数组,由String引用构成
String[,,] myStrings = new String[5, 3, 10];

交错数组(jagged array)

数组构成的数组。
// 创建由多个Point数组构成的一维数组
Point[][] myPolygons = new Point[3][];

示例——初始化数组
// 下面语句等效
String[] names = new String[] {"Aidan", "Grant"};
String[] names = {"Aidan", "Grant"};
var names = new String[] {"Aidan", "Grant"};
var names = new[] {"Aidan", "Grant"};
//var names = {"Aidan", "Grant"}; // 错误

示例——匿名类型数组
// 使用C#的隐式类型的局部变量、隐式类型的数组和匿名类型功能
var kids = new [] {new { Name="Aidan" }, new { Name="Grant" }};
foreach (var kid in kids)
	Console.WriteLine(kid.Name);

数组转型

示例——数组转型
// 创建二维FileStream数组
FileStream[,] fs2dim = new FileStream[5, 10];
// 隐式转型为二维Object数组
Object[,] o2dim = fs2dim;
// 显式转型为二维Stream数组
Stream[,] s2dim = (Stream[,]) o2dim;

// 创建一维Int32数组(元素是值类型)
Int32 ildim = new Int32[5];
//错误: 不能将值类型的数组转型为其他任何类型
//Object[] oldim = (Object[]) ildim;

// 创建一个新数组,使用Array.Copy将源数组中的每个元素
// 转型为目标数组中的元素,并把它们复制过去。
// 下面的代码创建元素为引用类型的数组,
// 每个元素都是对已装箱Int32的引用
// Array.Copy是浅复制
Object[] obldim = new Object[ildim.Length];
Array.Copy(ildim, obldim, ildim.Length);

Array.Copy 的作用不仅仅是将元素从一个数组复制到另一个。Copy 方法还能正确处理内存的重叠区域,就像C的 memmove 函数一样。有趣的是,C的 memcpy 函数反而不能正确处理重叠的内存区域。Copy 方法还能在复制每个数组元素时进行必要的类型转换,具体如下所述:

● 将值类型的元素装箱为引用类型的元素,比如将一个Int32[]复制到一个Object[]中。
● 将引用类型的元素拆箱为值类型的元素,比如将一个Object[]复制到一个Int32[]中。
● 加宽 CLR 基元值类型,比如将一个Int32的元素复制到一个Double[]中。
● 在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性,比如从 Object[]转型为 IFormattable[],就根据需要对元素进行向下类型转换。如果Object[]中的每个对象都实现了 IFormattable,Copy 方法就能成功执行。

示例——Array.Copy
// 定义实现了一个接口的值类型
internal struct MyValueType : IComparable {
	public Int32 CompareTo(Object obj) {
	
	}
}

public static class Program {
	public static void Main() {
		MyValueType[] src = new MyValueType[100];
		IComparable[] dest = new IComparable[src.Length];
		//初始化IComparable数组中的元素,
		//使它们引用源数组元素的已装箱版本
		Array.Copy(src, dest, src.Length);
	}
}

有时确实需要将数组从一种类型转换为另一种类型。这种功能称为数组协变性(array covariance)。但在利用它时要清楚由此而来的性能损失。

注意   如果只是需要将数组的某些元素复制到另一个数组,可选择 System.BufferBlockCopy 方法,它比 ArrayCopy 方法快。但 BufferBlockCopy 方法只支持基元类型,不提供像 ArrayCopy 方法那样的转型能力。方法的 Int32 参数代表的是数组中的字节偏移量,而非元素索引。设计 BlockCopy 的目的实际是将按位兼容(bitwise-compatible)的数据从一个数组类型复制到另一个按位兼容的数据类型,比如将包含Unicode 字符的一个Byte[](按字节的正确顺序)复制到一个Char[]中。该方法一定程度上弥补了不能将数组当作任意类型的内存块来处理的不足。
要将一个数组的元素可靠地复制到另一个数组,应该使用 System.ArrayConstrainedCopy 方法。该方法要么完成复制,要么抛出异常,总之不会破坏目标数组中的数据。这就允许 ConstrainedCopy 在约束执行区域(Constrained Execution Region,CER)中执行。为了提供这种保证,ConstrainedCopy 要求源数组的元素类型要么与目标数组的元素类型相同,要么派生自目标数组的元素类型。另外,它不执行任何装箱、拆箱或向下类型转换。

“按位兼容”因为英文原文是 bitwise-compatible,所以人们发明了 blittable 一词来表示这种类型。这 种类型在托管和非托管内存中具有相同的表示。一部分基元类型是 blittable 类型。如果一维数组包含 的是 blittable 类型,该数组也是 blittable 类型。另外,格式化的值类型如果只包含 blittable 类型,该值 类型也是 blittable 类型。欲知评情。请在MSDN文档中搜索"可直接复制到本机结构中的类型和非直接复制到本机结构中的类型"这一主题。——译注

所有数组都隐式派生自System.Array

所有数组都隐式实现IEnumerable, ICollection和IList

创建一维0基数组类型时,CLR自动使数组类型实现IEnumerable<T>, ICollection<T>和IList<T>(T是数组元素类型)。同时,还为数组类型的所有基类型实现这三个接口,只要它们是引用类型。

注意,如果数组包含值类型的元素,数组类型不会为元素的基类型实现接口。例如,如果执行以下代码:
DateTime[] dtArray; // 一个值类型的数组
那么,DateTime[]类型只会实现IEnumerable<DateTime>, ICollection<DateTime>和IList<DateTime>接口,不会为DateTime的基类型(包括System.ValueType和System.Object)实现这些泛型接口。这是因为值类型的数组在内存中的布局与引用类型的数组不同。

创建下限非零的数组

using System;

namespace ConsoleApp32
{
    class Program
    {
        static void Main(string[] args)
        {
            Int32[] lowerBounds = { 2005, 1 };
            Int32[] lengths = { 5, 4};
            Decimal[,] quarterlyRevenue = (Decimal[,])
                Array.CreateInstance(typeof(Decimal), lengths, lowerBounds);

            Console.WriteLine("{0,4} {1,9} {2,9} {3,9} {4,9}",
                "Year", "Q1", "Q2", "Q3", "Q4");
            //获取第1维第1个元素的索引
            Int32 firstYear = quarterlyRevenue.GetLowerBound(0);
            //获取第1维最后一个元素的索引
            Int32 lastYear = quarterlyRevenue.GetUpperBound(0);
            //获取第2维第1个元素的索引
            Int32 firstQuarter = quarterlyRevenue.GetLowerBound(1);
            //获取第2维最后一个元素的索引
            Int32 lastQuarter = quarterlyRevenue.GetUpperBound(1);

            for (Int32 year = firstYear; year <= lastYear; year++)
            {
                Console.Write(year + " ");
                for (Int32 quarter = firstQuarter; quarter <= lastQuarter; quarter++)
                {
                    Console.Write("{0,9:C} ", quarterlyRevenue[year, quarter]);
                }
                Console.WriteLine();
            }

            Console.ReadLine();
        }
    }
}
1111111.png

访问一维0基数组的元素比访问非0基一维或多维数组的元素稍快。

以下代码演示了访问二维数组的三种方式(安全、交错和不安全)

using System;
using System.Diagnostics;

namespace ConsoleApp32
{
    class Program
    {
        private const Int32 c_numElements = 10000;
        static void Main(string[] args)
        {
            // 声明二维数组
            Int32[,] a2Dim = new int[c_numElements, c_numElements];

            // 将二维数组声明为交错数组(向量构成的向量)
            Int32[][] aJagged = new Int32[c_numElements][];
            for (Int32 x = 0; x < c_numElements; x++)
                aJagged[x] = new Int32[c_numElements];

            // 1: 用普通的安全技术访问数组中的元素
            Safe2DimArrayAccess(a2Dim);

            // 2: 用交错数组技术访问数组中的元素
            SafeJaggedArrayAccess(aJagged);

            // 3: 用unsafe技术访问数组中的所有无线
            Unsafe2DimArrayAccess(a2Dim);

            Console.ReadLine();
        }

        private static Int32 Safe2DimArrayAccess(Int32[,] a)
        {
            Int32 sum = 0;
            for (Int32 x = 0; x < c_numElements; x++)
            {
                for (Int32 y = 0; y < c_numElements; y++)
                    sum += a[x, y];
            }
            return sum;
        }

        private static Int32 SafeJaggedArrayAccess(Int32[][] a)
        {
            Int32 sum = 0;
            for (Int32 x = 0; x < c_numElements; x++)
            {
                for (Int32 y = 0; y < c_numElements; y++)
                    sum += a[x][y];
            }
            return sum;
        }

        //C#编译器需指定/unsafe开关 ([项目属性页]->[生成]->允许不安全代码)
        //访问标记了unsafe修饰符,这是fixed语句所必须的。
        private static unsafe Int32 Unsafe2DimArrayAccess(Int32[,] a)
        {
            Int32 sum = 0;
            fixed (Int32* pi = a)
            {
                for (Int32 x = 0; x < c_numElements; x++)
                {
                    Int32 baseOfDim = x * c_numElements;
                    for (Int32 y = 0; y < c_numElements; y++)
                        sum += pi[baseOfDim + y];
                }
            }
            return sum;
        }
    }
}


Unsafte2DimArrayAccess方法标记了 unsafe 修饰符,这是使用 C#的 fixed 语句所必须的。编译这段代码要求在运行 C#编译器时指定/unsafe 开关,或者在 Microsoft Visual Studio 的项目属性页的“生成”选项卡中勾选“允许不安全代码”。

写代码时,不安全数据访问技术有时或许是你的最佳选择,但要注意该技术的三处不足。

● 相较于其他技术,处理数组元素的代码更复杂,不容易读和写,因为要使用 C# fixed 语句,要执行内存地址计算。
● 计算过程中出错,可能访问到不属于数组的内存。这会造成计算错误,损坏内存数据,破坏类型安全性,并可能造成安全漏洞。
● 因为这些潜在的问题,CLR 禁止在降低了安全级别的环境(如 Microsoft Silverlight)中运行不安全代码。

不安全的数组访问和固定大小的数组

不安全的数组访问非常强大,因为它允许访问以下元素。

● 堆上的托管数组对象中的元素(上一节对此进行了演示)。
● 非托管堆上的数组中的元素。SecureString 示例演示了如何调用 System.Runtime.InteropServices.Marshal 类的 SecureStringToCoTaskMemUnicode 方法来返回一个数组,并对这个数组进行不安全的数组访问。
● 线程栈上的数组中的元素。

如果性能是首要目标,请避免在堆上分配托管的数组对象。相反,在线程栈上分配数组。这是通过C#的 stackalloc 语句来完成的(它在很大程度上类似于C的 alloca 函数), stackalloc语句只能创建一维 0 基、由值类型元素构成的数组,而且值类型绝对不能包含任何引用类型的字段。实际上,应该把它的作用看成是分配一个内存块,这个内存块可以使用不安全的指针来操纵。所以,不能将这个内存缓冲区的地址传给大部分 FCL 方法。当然,栈上分配的内存(数组)会在方法返回时自动释放;这对增强性能起了一定作用。使用这个功能要求为 C#编译器指定/unsafe 开关。

以下代码演示了如何使用C# stackalloc 语句

using System;

namespace ConsoleApp32
{
    class Program
    {
        static void Main(string[] args)
        {
            StackallocDemo();
            InlineArrayDemo();
            Console.ReadLine();
        }

        private static void StackallocDemo()
        {
            unsafe
            {
                const Int32 width = 20;
                Char* pc = stackalloc Char[width]; // 在栈上分配数组

                String s = "Jeffrey Richter"; // 15个字符
                
                for (Int32 index = 0; index < width; index++)
                {
                    pc[width - index - 1] = (index < s.Length) ? s[index] : '.';
                }
                // 下面这行代码显示".....rethciR yerffeJ"
                Console.WriteLine(new String(pc, 0, width));
            }
        }

        private static void InlineArrayDemo()
        {
            unsafe
            {
                CharArray ca; // 在栈上分配数组
                Int32 widthInBytes = sizeof(CharArray);
                Int32 width = widthInBytes / 2;

                String s = "Jeffrey Richter"; // 15个字符

                for (Int32 index = 0; index < width; index++)
                {
                    ca.Characters[width - index - 1] =
                        (index < s.Length) ? s[index] : '.';
                }
                // 下面这行代码显示".....rethciR yerffeJ"
                Console.WriteLine(new String(ca.Characters, 0, width));
            }
        }
    }

    internal unsafe struct CharArray
    {
        // 这个数组内联(嵌入)到结构中
        public fixed Char Characters[20];
    }
}
22222.png


通常,由于数组是引用类型,所以结构中定义的数组字段实际只是指向数组的指针或引用;数组本身在结构的内存的外部。不过,也可像上述代码中的 CharArray结构那样,直接将数组嵌入结构。在结构中嵌入数组需满足以下几个条件。

● 类型必须是结构(值类型);不能在类(引用类型)中嵌入数组。
● 字段或其定义结构必须用 unsafe 关键字标记。
● 数组字段必须用 fixed 关键字标记。
● 数组必须是一维 0 基数组。
● 数组的元素类型必须是以下类型之一:Boolean,Char,SByte,Byte, Int16,Int32, UIntl6,UInt32,Int64,UInt64,SingleDouble

要和非托管代码进行互操作,而且非托管数据结构也有一个内联数组,就特别适合使用内联的数组。但内联数组也能用于其他地方。上述代码中的 InlineArrayDemo 方法提供了如何使用内联数组的一个例子。它执行和 StackallocDemo 方法一样的功能,只是用了不一样的方式。

评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容