#Updated【2022.7.29 替换文中不清晰的代码图片】
#Updated【2022.7.30 修正已知错误,补充相关说明】
数组,一种数据类型(在绝大数语言中不是基本数据类型)且为引用类型,在内存中以连续的内存单元进行分配,所以其大小在创建对象后为定值,不可更改。
对于两种不同数据类型而言,其内存分配方式是不同的。值类型直接在栈(C#中称为堆栈Stack)上分配,将其储存的内容直接存放到栈中;引用类型则是将指向实例对象的地址存在栈中,对该地址进行解析后获得一个在堆(此处,C#中称为托管堆)中的位置,这个位置储存着真正的内容。

对于int类型,其大小为32位,即4个字节,所以整型数组中的每个元素占4个字节。但整个数组在内存中所占字节大小并不是“Length * 4”,因为在位数组分配的内存开头会被同步索引块、类型对象指针和数组长度所占据。
一般地:对于32位程序,上述三者分别占用4个字节;64位程序则分别占用8个字节。
在内存分配表中可以看到:(32位程序)
第一行表示同步索引块;第二行表示类型对象指针;第三行表示当前数组所分配的长度;第四行目前本人不知道其具体含义,欢迎大佬留言;之后便是存储的每个元素,每个元素占4个字节。
【注:上述详细内容请查找CLR相关资料】
据此,可以推断出:我们显示看到的长度指的是可储存元素的数量,而数组的整体大小则需要额外加上16字节(32位)/ 32字节(64位)。
对于数组的访问,一般采用索引的方式访问每一个元素。本人看来,索引读取的内容是内存地址。数组之所以可以用索引来访问,是因为其在内存中是连续的,就像C/C++中用指针访问数组,指针加一即表示下一个元素。此外,由于数组满足通过索引访问,索引由整形数字表示,所以理论上,数组最大长度为整型最大值(int.MaxValue = 2^31 – 1 = 2147483647),但由于实际内存的原因,一般地,一维最好不要超过10^7,二维最好不要超过10^5 * 10^5。
长度索引模式要求被访问对象具有可确定的长度且支持索引运算符[],如数组和集合(List等);而某些数据结构在运行时长度未知,某些不支持索引访问,如Stack<T>、Queue<T> 和 Dictionary<TKey and TValue>等。
对于迭代器模式,其实现方式基于IEnumerator<T>接口,也就是说能够使用foreach进行访问的对象/类型,必须可以创建/返回该接口的对象。
【注:C#不要求必须实现IEnumerable/IEnumerable<T>才能使用foreach迭代数据类型,只要求包含GetEnumerator(可返回类型为IEnumerator<T>的对象)的公共定义即可】

IEnumerator<T>接口对应的类型是一个集合访问器,即可迭代对象,其内部包含一个属性Current,用于返回当前处理的元素;两个方法(bool)MoveNext,从一个元素移到下一个元素,同时检测是否已枚举完所有项;Reset,通常会抛出 NotImplementedException,是在无法实现请求的方法或操作时引发的异常。
就数组而言对于上述两种模式的效率差异不是很大。平均地,除第一次外,索引访问要快于迭代器访问。

对于IEnumerator<T>而言,在多重循环、多线程以及共享状态下,如果两个foreach彼此交错且访问的是同一个集合,那就会出现一个枚举器被多条语句使用的情况。集合必须始终有当前元素的状态指示符,以便在调用MoveNext时,可以确定下一个元素。在这种情况下,交错的一个循环可能会影响另一个循环。
所以,集合类不直接支持IEnumerator<T>和IEnumerator接口。而是直接支持另一种接口IEnumerable<T>。该接口的作用是返回一个类型为IEnumerator<T>的对象。这样不同的迭代语句有着自己的迭代器,不会相互争抢。
当然,官方应该没有这样的称法,只是称一维、二维数组等。
对于一维数组,其在内存中就是简单的以连续的方式进行存储。理论上,其最大长度为2147483647,但实测后其最大长度为2147483591(实测平台:.NET 6 32位控制台程序)。
对于二维或更高维的数组,其在内存中的存储方式并不是我们所想象的一个矩阵或一个立方,内存只由一个个独立的存储单元组成,并不能表示出具有相关性的几何区域。

在内存分配表中可以看到:(32位程序)
一维数组占用前16字节,存储数组本体信息;而二维数组占用32字节存储本体信息。之后的数据存储方式与一维数组一致,每个元素占4个字节。由此可以推断出多维数组的存储方式依旧是按连续内存的方式进行存储。
#一维数组和多维数组的下标转换#
(1)多维 => 一维(扁平化处理)
int[,,,,…,] arr = new int[a, b, c, d,…,k];
arr[x, y, z, w,…,n] => idx = x * (b * c * d * …) + y * (c * d * …) + z * (d * …) + … + n
(2)一维 => 多维(以二维为例)
int[,] arr = new int[a, b];
p[x] = arr[u, v] 其中u = x / b v = x % b
以数组为元素的数组,叫做交错数组,即该数组内部元素为数组,每个数组的大小可以不同。

以上时两种基本初始化方式。在初始化交错数组时,第一个索引运算符表示长度,第二个索引运算符表示维度,如一维[],二维[,]等。
元素的访问与之前提到的方法类似,通过单个索引运算符访问
第一个索引运算符表示arr中的对象,第二个索引运算符表示对应对象中的元素。
【注:本节提到的源码及内容均在.NET 5的基础上论述】

通过反编译发现,该方法存在的16个重载最后都会调用Line 2125的方法。

【以下内容为源码分析】
(1) Nullable<T> 表示可被分配为null的值类型,[Nullable(type)]表示被修饰的数据结构可以存储type类型的元素,没有值则存储null;
(2) keys 表示目标数组,即待排序的数组;
(3) items 表示另一个数组(默认为null),其内部的每一个元素与keys中每个关键字对应;常用于两个数组的关联排序,默认将keys中的元素按索引顺序和items中的元素一一对应,在排序时以keys中的元素为比较对象进行排序,在对keys中的元素进行位置移动时会连带对应items中的元素一起移动,类似于键值对。
(4) index:排序起始索引(默认为0);
(5) length:排序长度(默认为arr.Length);
(6) compare:比较器对象(默认为升序)。
(1) keys.Length - (index - lowerBound) < length 表示 从index开始的要排序的元素长度 小于 标称长度length,即要排序的元素长度不够;
(2) items != null && index – lowerBound > items.Length – length 表示 index之前的不参与排序元素长度 已经超过 items的长度,使得keys中要排序的部分没有可对应的items元素。

(1) as 关键字判断并进行转换。若无法转换且不发生错误,则返回null;否则返回转换后得到对象;
(2) Line 2161:array != null 表示该数组成功转换为object类型;
(3) Line 2164:表示items为空 或 items成功转换;
【注:(4)(5)为本人结合资料推断得出,有待证实】
(4) as成功转换的前提是,当需要转化对象的类型属于转换目标类型 或者 属于转换目标类型的派生类型时,转换操作才能成功,而且并不产生新的对象。而数组类型在System.Array中,并不属于System.Object,所以自带的数组类型无法利用as转换为object类型。
因此,可推断:所有基本数据类型均不能以上述方式成功转换,因为基本数据类型属于System.ValueType;而自定义的对象数据类型可以成功转换,因为自定义类型属于Sytem.Object。
(5) 如果全都转换成功,则需要利用对象的排序方式进行排序,不能使用基本数据类型的方式进行排序。(体现在Line 2166与Line 2215)。


之后将keys转换为Span列表,Span类型可以表示任意内存的相邻区域,以此达到部分排序的目的;Span中的Sort方法位于MemoryExtensions类中。


(1) 内部的Default对象会根据是否实现了接口IComparable<T>来创建不同的 ArraySortHelper;
(2) Type.IsAssignbleFrom(Type c)方法判读指定类型c的实例是否能分配给当前类型Type的变量;即判断c是否为Type类型或其派生类型。

【至此,数组开始正式进入排序阶段】

(插排、堆排代码如下):

其中的DownHeap是建立顶堆的过程,默认为小顶堆。
1. .NET对数组进行排序时,有较长的“前摇”,需要判断、转换等相关操作;
2. .NET中对数组的排序方法不是单一的,而是综合许多排序方法,在不同条件下选择不同的方法,以达到最优的解法。


一个集合的浅度拷贝意味着只拷贝集合中的元素,不管他们是引用类型或者是值类型,不拷贝引用所指的对象。即,新集合中的引用和原始集合中的引用所指的对象是同一个对象。与此形成对比的是深度拷贝,不仅拷贝集合中的元素,而且还拷贝元素直接或者间接引用的所有东西。即,新集合中的引用和原始集合中的引用所指的对象是不同的。

【注:下方内容为本人结合资料推断得出,有待证实】

将传入的对象的Flags属性与2^24做且运算 和 (uint)0无符号整数0进行比较。之后按照不同的情况进行数据填充,完成后返回类型为object的对象。

接下来的过程与Clone方法基本一致。

该部分主要功能是抛出异常,因为在实际使用中,无法调用到包含bool reliable参数的这个方法。



可以发现,其原理和Copy方法、Clone方法基本一致。
1. 三种方法均可以将一个数组的内容,放到另一个数组上。
2. Clone方法具有返回值,为Object类型,在克隆后直接赋值给目标数组,因此不需要目标数组实例化。
2. Copy与CopyTo方法没有返回值,是通过直接在目标数组上填充,以完成复制,因此目标数组必须实例化且目标数组必须和源数组类型一致。
4. Copy为静态方法,可通过Array类名直接调用;Clone与CopyTo方法为非静态方法,故需要实例化的一个数组来调用。
5. Clone方法使浅层拷贝,Copy与CopyTo是深层拷贝。 (该推论存在错误,后文已进行相应补充说明)。



1. 本文从源码的角度,对数组、数组遍历以及常见方法进行了分析与论述。更多关于数组的内容可参阅(.NET API 浏览器 | Microsoft Docs)。
2. 对于内存分配,数组属于引用类型,其值储存在堆中,但引用的对象储存在栈中(是一个内存地址),通过该引用对象找到并访问堆中的值。
3. 对于迭代器访问,所以可使用迭代器访问的数据类型均必须可以创建或返回一个IEnumerator<T>的对象,供迭代器使用。
4. 对于Array.Sort()方法,其内部不是单一的排序方式,而是在不同情况下使用不同的的排序方式,以达到最佳效率。
5. 对于三种复制方法,Clone为浅层拷贝,拷贝后,新数组与源数组引用地址相同;Copy与CopyTo为深层拷贝,拷贝后,新数组与源数组引用地址不同,是一个完全新的对象。
#Updated【2022.7.30补充内容】【在此,感谢大佬 a1010 指出错误】
一般的,对于值类型而言,无论是浅层还是深层复制,均会开辟新的存储空间。对于引用类型,浅层复制时,新数据与源数据的引用一致;深层复制时,新数据的引用类型变量会开辟新的存储空间。
对于上方提到的三种拷贝方式最终推出的结论和三幅有关引用地址的图片,均其实存在错误。
1. 三种拷贝方式,拷贝的是内容,并不是对象本身。所以从上方的三幅图片来看,拷贝前后的对象(即,数组本身)引用地址本就是不同的,无法说明哪种方式是浅层复制,还是深层复制。
2. 此处用于测试的数组类型为Int32整型,内部元素属于值类型,不受拷贝方式的影响。所以三种拷贝方式无法区分出具体的复制方式。
【注:以下所有测试均在 .NET 6 32位程序 环境下进行】



上方三图也证实了值类型的拷贝不受复制方式的影响这一推论。



由于,字符串类型的特殊优化性,当字符串内容相同时,总是指向同一个实例对象。所以无论何种拷贝方式,最终均指向同一个实例。

结构体属于值类型,从刚才的推论看,其本身不会受拷贝方式的影响,在拷贝后,均会创建新的存储单元。而内部成员一个为值类型,一个为string类型,由于string的特殊性,总会指向同一个实例对象,所以地址应该也不受拷贝方式影响。



可发现,其结果和预测相同。即,数组本身引用因为是值类型所以引用不同。

类属于引用类型,根据推断其引用会受拷贝方式的影响。



据上方三幅图片显示,无论那种拷贝方式,最终数组成员本身的引用对象均指向同一个实例,均属于浅层复制。
回到三种拷贝方式的源码,可以发现,无论是标称就是浅层复制的Clone()方法,还是另外两种方法,均会进入到两个方法中:
Clone()方法:

Copy()方法:

CopyTo()方法:

可以发现,无论那种方法,最终要么进入到BulkMoveWithWriteBarrier方法,要么进入Memmove方法。
这两个方法均属于Buffer类,该类主要用于操作基元类型的数组。所谓基元类型指:编译器直接支持的类型称为基元类型,可以直接映射到 FCL 中存在的类型(.NET Framework 类库)——百度百科。说人话就是.NET类库中默认存在的数据类型。

操作基元类型的数组,根据上表可以推断出也就是可合法定义出的所有数组。
在该方法内部又有两个去向,一个是__BulkMoveWithWriteBarrier方法(开头下划线较长),另一个是_BulkMoveWithWriteBarrier方法(下划线较短)。

而在_BulkMoveWithWriteBarrier方法(下划线较短)中,经过一系列操作后,最终又会进入到__BulkMoveWithWriteBarrier方法(下划线较长)中。

对于__BulkMoveWithWriteBarrier方法(下划线较长),其是一个扩展方法,具体实现在外部。
另一个Memmove方法也是如此,最终会进入到一个扩展方法中。

目前本人对于扩展方法的使用及相关知识掌握不足,所以源码的分析也不够透彻。至此可以得到,在数组中,三种方式所使用的拷贝方法在复制方式并没有什么区别,其区别主要在于返回类型、方法是否为静态和对目标数组的要求。
值得注意的是,似乎只有Clone()方法,强制要求目标数组与源数组维度相同,其他两种方法没有这一要求。


1. Array中的拷贝方法似乎并不能体现出浅层复制与深层复制。
2. 要体现出这两种复制的区别似乎需要在其他数据类型中体现,且存在简单对象与复杂对象的区别。
3. 目前(.NET 6)所能实现深层复制的主流方法是利用反射实现、利用二进制化和反序列化,这两种方法今后有机会将会提到。
4. 关于这两种复制方法,以及简单与复杂对象,本人将继续深入研究。
【感谢您可以抽出时间阅读到这里,因个人水平有限,可能存在错误,望各位大佬指正,留下宝贵意见,谢谢!】
作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
我的代码目前看起来像这样numbers=[1,2,3,4,5]defpop_threepop=[]3.times{pop有没有办法在一行中完成pop_three方法中的内容?我基本上想做类似numbers.slice(0,3)的事情,但要删除切片中的数组项。嗯...嗯,我想我刚刚意识到我可以试试slice! 最佳答案 是numbers.pop(3)或者numbers.shift(3)如果你想要另一边。 关于ruby-多次弹出/移动ruby数组,我们在StackOverflow上找到一
我需要读入一个包含数字列表的文件。此代码读取文件并将其放入二维数组中。现在我需要获取数组中所有数字的平均值,但我需要将数组的内容更改为int。有什么想法可以将to_i方法放在哪里吗?ClassTerraindefinitializefile_name@input=IO.readlines(file_name)#readinfile@size=@input[0].to_i@land=[@size]x=1whilex 最佳答案 只需将数组映射为整数:@land边注如果你想得到一条线的平均值,你可以这样做:values=@input[x]
我正在使用puppet为ruby程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这
是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou
这个问题在这里已经有了答案:Checktoseeifanarrayisalreadysorted?(8个答案)关闭9年前。我只是想知道是否有办法检查数组是否在增加?这是我的解决方案,但我正在寻找更漂亮的方法:n=-1@arr.flatten.each{|e|returnfalseife
我有一个这样的哈希数组:[{:foo=>2,:date=>Sat,01Sep2014},{:foo2=>2,:date=>Sat,02Sep2014},{:foo3=>3,:date=>Sat,01Sep2014},{:foo4=>4,:date=>Sat,03Sep2014},{:foo5=>5,:date=>Sat,02Sep2014}]如果:date相同,我想合并哈希值。我对上面数组的期望是:[{:foo=>2,:foo3=>3,:date=>Sat,01Sep2014},{:foo2=>2,:foo5=>5:date=>Sat,02Sep2014},{:foo4=>4,:dat