如何填充/实例化一个c#数组与单一的值?

我知道c#中实例化的值类型数组会自动用类型的默认值填充(例如bool为false, int为0,等等)。

是否有一种方法来自动填充一个不是默认的种子值的数组?无论是在创建时还是在创建后使用内置方法(如Java的Arrays.fill ())?假设我想要一个默认为true的布尔数组,而不是false。是否有一个内置的方法来做到这一点,或者你只需要通过一个for循环迭代数组?

 // Example pseudo-code:
bool[] abValues = new[1000000];
Array.Populate(abValues, true);


// Currently how I'm handling this:
bool[] abValues = new[1000000];
for (int i = 0; i < 1000000; i++)
{
abValues[i] = true;
}

必须遍历数组并将每个值“重置”为true似乎效率不高。还有其他方法吗?也许通过翻转所有值?

在输入这个问题并思考之后,我猜默认值只是c#在幕后处理这些对象的内存分配的结果,所以我想这可能是不可能的。但我还是想确定一下!

341346 次浏览

我不知道框架方法,但你可以写一个快速助手来帮你做。

public static void Populate<T>(this T[] arr, T value ) {
for ( int i = 0; i < arr.Length;i++ ) {
arr[i] = value;
}
}

创建一个包含一千个true值的新数组:

var items = Enumerable.Repeat<bool>(true, 1000).ToArray();  // Or ToList(), etc.

类似地,你可以生成整数序列:

var items = Enumerable.Range(0, 1000).ToArray();  // 0..999

在谷歌搜索和阅读之后,我发现了这个:

bool[] bPrimes = new bool[1000000];
bPrimes = Array.ConvertAll<bool, bool>(bPrimes, b=> b=true);

这肯定更接近我要找的东西。但我不确定这是否比在for循环中遍历原始数组并只更改值更好。事实上,经过快速测试后,它看起来慢了大约5倍。所以这不是一个好的解决方案!

Enumerable.Repeat(true, 1000000).ToArray();

不幸的是,我不认为有一个直接的方法,但我认为你可以为数组类写一个扩展方法来做到这一点

class Program
{
static void Main(string[] args)
{
int[] arr = new int[1000];
arr.Init(10);
Array.ForEach(arr, Console.WriteLine);
}
}


public static class ArrayExtensions
{
public static void Init<T>(this T[] array, T defaultVaue)
{
if (array == null)
return;
for (int i = 0; i < array.Length; i++)
{
array[i] = defaultVaue;
}
}
}

这也是可行的……但可能没有必要

 bool[] abValues = new bool[1000];
abValues = abValues.Select( n => n = true ).ToArray<bool>();

对于大型数组或可变大小的数组,您可能应该使用:

Enumerable.Repeat(true, 1000000).ToArray();

对于小数组,你可以使用c# 3中的集合初始化语法:

bool[] vals = new bool[]{ false, false, false, false, false, false, false };

集合初始化语法的好处是,不必在每个槽中使用相同的值,可以使用表达式或函数初始化一个槽。另外,我认为您避免了将数组槽初始化为默认值的代价。举个例子:

bool[] vals = new bool[]{ false, true, false, !(a ||b) && c, SomeBoolMethod() };

如果你的数组太大,你应该使用BitArray。它为每个bool值使用1位,而不是一个字节(就像在bool数组中一样),您还可以使用位操作符将所有位设置为true。或者在true上初始化。如果你只需要做一次,它只会花费更多。

System.Collections.BitArray falses = new System.Collections.BitArray(100000, false);
System.Collections.BitArray trues = new System.Collections.BitArray(100000, true);


// Now both contain only true values.
falses.And(trues);

并行实现怎么样

public static void InitializeArray<T>(T[] array, T value)
{
var cores = Environment.ProcessorCount;


ArraySegment<T>[] segments = new ArraySegment<T>[cores];


var step = array.Length / cores;
for (int i = 0; i < cores; i++)
{
segments[i] = new ArraySegment<T>(array, i * step, step);
}
var remaining = array.Length % cores;
if (remaining != 0)
{
var lastIndex = segments.Length - 1;
segments[lastIndex] = new ArraySegment<T>(array, lastIndex * step, array.Length - (lastIndex * step));
}


var initializers = new Task[cores];
for (int i = 0; i < cores; i++)
{
var index = i;
var t = new Task(() =>
{
var s = segments[index];
for (int j = 0; j < s.Count; j++)
{
array[j + s.Offset] = value;
}
});
initializers[i] = t;
t.Start();
}


Task.WaitAll(initializers);
}

当只初始化一个数组时,这段代码的力量是看不出来的,但我认为你绝对应该忘记“纯”for。

如果你计划只设置数组中的几个值,但大多数时候想要获得(自定义)默认值,你可以尝试这样做:

public class SparseArray<T>
{
private Dictionary<int, T> values = new Dictionary<int, T>();


private T defaultValue;


public SparseArray(T defaultValue)
{
this.defaultValue = defaultValue;
}


public T this [int index]
{
set { values[index] = value; }
get { return values.ContainsKey(index) ? values[index] ? defaultValue; }
}
}

你可能需要实现其他接口来让它有用,比如数组本身上的接口。

还是……您可以简单地使用反向逻辑。设false表示true,反之亦然。

代码示例

// bool[] isVisible = Enumerable.Repeat(true, 1000000).ToArray();
bool[] isHidden = new bool[1000000]; // Crazy-fast initialization!


// if (isVisible.All(v => v))
if (isHidden.All(v => !v))
{
// Do stuff!
}

关于这个(重复的?)问题有更多的答案:在c#中,memset的等价物是什么?

有人已经对替代方案进行了基准测试(他们包括一个不安全的版本,但他们没有尝试memset): http://techmikael.blogspot.co.uk/2009/12/filling-array-with-default-value.html

下面的代码结合了小副本的简单迭代和Array。复印大份

    public static void Populate<T>( T[] array, int startIndex, int count, T value ) {
if ( array == null ) {
throw new ArgumentNullException( "array" );
}
if ( (uint)startIndex >= array.Length ) {
throw new ArgumentOutOfRangeException( "startIndex", "" );
}
if ( count < 0 || ( (uint)( startIndex + count ) > array.Length ) ) {
throw new ArgumentOutOfRangeException( "count", "" );
}
const int Gap = 16;
int i = startIndex;


if ( count <= Gap * 2 ) {
while ( count > 0 ) {
array[ i ] = value;
count--;
i++;
}
return;
}
int aval = Gap;
count -= Gap;


do {
array[ i ] = value;
i++;
--aval;
} while ( aval > 0 );


aval = Gap;
while ( true ) {
Array.Copy( array, startIndex, array, i, aval );
i += aval;
count -= aval;
aval *= 2;
if ( count <= aval ) {
Array.Copy( array, startIndex, array, i, count );
break;
}
}
}

使用int[]数组的不同数组长度的基准是:

         2 Iterate:     1981 Populate:     2845
4 Iterate:     2678 Populate:     3915
8 Iterate:     4026 Populate:     6592
16 Iterate:     6825 Populate:    10269
32 Iterate:    16766 Populate:    18786
64 Iterate:    27120 Populate:    35187
128 Iterate:    49769 Populate:    53133
256 Iterate:   100099 Populate:    71709
512 Iterate:   184722 Populate:   107933
1024 Iterate:   363727 Populate:   126389
2048 Iterate:   710963 Populate:   220152
4096 Iterate:  1419732 Populate:   291860
8192 Iterate:  2854372 Populate:   685834
16384 Iterate:  5703108 Populate:  1444185
32768 Iterate: 11396999 Populate:  3210109
第一列是数组大小,然后是使用简单迭代(@ jaredared实现)复制的时间。此方法的时间在此之后。 这些基准测试使用了一个包含四个整数

的结构数组
         2 Iterate:     2473 Populate:     4589
4 Iterate:     3966 Populate:     6081
8 Iterate:     7326 Populate:     9050
16 Iterate:    14606 Populate:    16114
32 Iterate:    29170 Populate:    31473
64 Iterate:    57117 Populate:    52079
128 Iterate:   112927 Populate:    75503
256 Iterate:   226767 Populate:   133276
512 Iterate:   447424 Populate:   165912
1024 Iterate:   890158 Populate:   367087
2048 Iterate:  1786918 Populate:   492909
4096 Iterate:  3570919 Populate:  1623861
8192 Iterate:  7136554 Populate:  2857678
16384 Iterate: 14258354 Populate:  6437759
32768 Iterate: 28351852 Populate: 12843259
Boolean[] data = new Boolean[25];


new Action<Boolean[]>((p) => { BitArray seed = new BitArray(p.Length, true); seed.CopyTo(p, 0); }).Invoke(data);

没有办法将数组中的所有元素设置为单个操作,除非,该值是元素类型的默认值。

例如,如果它是一个整数数组,你可以通过一个操作将它们全部设置为0,如下所示: Array.Clear(...) < / p >

我知道我来晚了,但我有个主意。编写一个包装器,其中包含与被包装值之间的转换操作符,以便它可以用作被包装类型的替身。这实际上是受到@l33t的愚蠢回答的启发。

首先(来自c++),我意识到在c#中,当数组的元素被构造时,默认的ctor是不被调用的。相反,即使存在用户定义的默认构造函数!——所有数组元素都是零初始化的。这确实让我大吃一惊。

因此,包装器类只提供一个默认的ctor和所需的值,就可以用于c++中的数组,但不适用于c#。一种解决方法是让包装器类型在转换时将0映射到所需的种子值。这样一来,在所有实际应用中,零初始化值似乎都被种子初始化了:

public struct MyBool
{
private bool _invertedValue;


public MyBool(bool b)
{
_invertedValue = !b;
}


public static implicit operator MyBool(bool b)
{
return new MyBool(b);
}


public static implicit operator bool(MyBool mb)
{
return !mb._invertedValue;
}


}


static void Main(string[] args)
{
MyBool mb = false; // should expose false.
Console.Out.WriteLine("false init gives false: "
+ !mb);


MyBool[] fakeBoolArray = new MyBool[100];


Console.Out.WriteLine("Default array elems are true: "
+ fakeBoolArray.All(b => b) );


fakeBoolArray[21] = false;
Console.Out.WriteLine("Assigning false worked: "
+ !fakeBoolArray[21]);


fakeBoolArray[21] = true;
// Should define ToString() on a MyBool,
// hence the !! to force bool
Console.Out.WriteLine("Assigning true again worked: "
+ !!fakeBoolArray[21]);
}

此模式适用于所有值类型。例如,如果需要初始化4,则可以将int类型的0映射到4。

我很想像在c++中那样做一个模板,提供种子值作为模板参数,但我知道这在c#中是不可能的。还是我遗漏了什么?(当然,在c++中,映射根本不是必需的,因为可以提供一个默认的ctor,它将被数组元素调用。)

FWIW,这里有一个等价的c++: https://ideone.com/wG8yEh

如果你可以反转你的逻辑,你可以使用Array.Clear()方法将布尔数组设置为false。

        int upperLimit = 21;
double optimizeMe = Math.Sqrt(upperLimit);


bool[] seiveContainer = new bool[upperLimit];
Array.Clear(seiveContainer, 0, upperLimit);

下面是System.Collections.BitArray的另一个方法,它有这样的构造函数。

bool[] result = new BitArray(1000000, true).Cast<bool>().ToArray();

bool[] result = new bool[1000000];
new BitArray(1000000, true).CopyTo(result, 0);

在你创建数组的地方创建一个私有类,它有一个getter和setter。除非你需要数组中的每个位置都是唯一的,比如随机,然后使用int?作为一个数组,然后get如果位置等于空,则填充该位置并返回新的随机值。

IsVisibleHandler
{


private bool[] b = new bool[10000];


public bool GetIsVisible(int x)
{
return !b[x]
}


public void SetIsVisibleTrueAt(int x)
{
b[x] = false //!true
}
}

或使用

public void SetIsVisibleAt(int x, bool isTrue)
{
b[x] = !isTrue;
}

setter。

你可以在. net Core 2.0+和. net Standard 2.1+中使用Array.Fill

这里给出的许多答案都可以归结为一个循环,每次初始化数组中的一个元素,它没有利用设计为一次操作内存块的CPU指令。

. net Standard 2.1(在撰写本文时的预览版中)提供了Array.Fill (),这有助于在运行时库中实现高性能(尽管到目前为止,. net Core 似乎没有利用了这种可能性)。

对于较早的平台,当数组大小很大时,下面的扩展方法比普通循环的性能要好得多。当我的在线代码挑战的解决方案超过分配的时间预算20%时,我创建了它。它减少了大约70%的运行时间。在本例中,数组填充在另一个循环中执行。BLOCK_SIZE是根据直觉而不是实验设定的。一些优化是可能的(例如,复制所有已经设置为所需值的字节,而不是固定大小的块)。

internal const int BLOCK_SIZE = 256;
public static void Fill<T>(this T[] array, T value)
{
if (array.Length < 2 * BLOCK_SIZE)
{
for (int i = 0; i < array.Length; i++) array[i] = value;
}
else
{
int fullBlocks = array.Length / BLOCK_SIZE;
// Initialize first block
for (int j = 0; j < BLOCK_SIZE; j++) array[j] = value;
// Copy successive full blocks
for (int blk = 1; blk < fullBlocks; blk++)
{
Array.Copy(array, 0, array, blk * BLOCK_SIZE, BLOCK_SIZE);
}


for (int rem = fullBlocks * BLOCK_SIZE; rem < array.Length; rem++)
{
array[rem] = value;
}
}
}

如果你使用的是。net Core, . net Standard >= 2.1,或者依赖于系统。内存包,你也可以使用Span<T>.Fill()方法:

var valueToFill = 165;
var data = new int[100];


data.AsSpan().Fill(valueToFill);


// print array content
for (int i = 0; i < data.Length; i++)
{
Console.WriteLine(data[i]);
}

https://dotnetfiddle.net/UsJ9bu

下面是另一个被微软抛弃的版本。它的速度是Array.Clear的4倍,比帕诺斯·Theof的解决方案埃里克J的佩塔尔·彼得罗夫的同类快——对于大型数组,速度可以快两倍。

首先,我想向您介绍函数的祖先,因为这样更容易理解代码。在性能方面,这与Panos Theof的代码相当,对于某些事情来说可能已经足够了:

public static void Fill<T> (T[] array, int count, T value, int threshold = 32)
{
if (threshold <= 0)
throw new ArgumentException("threshold");


int current_size = 0, keep_looping_up_to = Math.Min(count, threshold);


while (current_size < keep_looping_up_to)
array[current_size++] = value;


for (int at_least_half = (count + 1) >> 1; current_size < at_least_half; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);


Array.Copy(array, 0, array, current_size, count - current_size);
}

如您所见,这是基于已初始化部分的重复加倍。这是简单而有效的,但它与现代内存架构相冲突。因此诞生了一个版本,它只使用加倍来创建一个缓存友好的种子块,然后在目标区域迭代地爆破:

const int ARRAY_COPY_THRESHOLD = 32;  // 16 ... 64 work equally well for all tested constellations
const int L1_CACHE_SIZE = 1 << 15;


public static void Fill<T> (T[] array, int count, T value, int element_size)
{
int current_size = 0, keep_looping_up_to = Math.Min(count, ARRAY_COPY_THRESHOLD);


while (current_size < keep_looping_up_to)
array[current_size++] = value;


int block_size = L1_CACHE_SIZE / element_size / 2;
int keep_doubling_up_to = Math.Min(block_size, count >> 1);


for ( ; current_size < keep_doubling_up_to; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);


for (int enough = count - block_size; current_size < enough; current_size += block_size)
Array.Copy(array, 0, array, current_size, block_size);


Array.Copy(array, 0, array, current_size, count - current_size);
}

注意:前面的代码需要(count + 1) >> 1作为加倍循环的限制,以确保最后的复制操作有足够的内容来覆盖所有剩余的内容。如果使用count >> 1来代替奇数,则不会出现这种情况。对于当前版本,这是没有意义的,因为线性复制循环将弥补任何懈怠。

数组单元格的大小必须作为参数传递,因为——令人难以置信的是——泛型不允许使用sizeof,除非它们使用约束(unmanaged),而约束在将来可能可用,也可能不可用。错误的估计不是什么大问题,但如果值是准确的,性能是最好的,原因如下:

  • 低估元素大小可能导致块大小超过L1缓存的一半,因此增加了从L1中删除复制源数据的可能性,并且必须从较慢的缓存级别重新获取。

  • 高估元素大小会导致CPU L1缓存利用率不足,这意味着线性块复制循环的执行次数比最佳利用率时要多。因此,产生的固定循环/调用开销比严格需要的要多。

下面是我的代码与Array.Clear和前面提到的其他三个解决方案的基准测试。计时用于填充给定大小的整数数组(Int32[])。为了减少缓存异常等引起的变化,每个测试执行两次,背靠背,并在第二次执行时进行计时。

array size   Array.Clear      Eric J.   Panos Theof  Petar Petrov   Darth Gizka
-------------------------------------------------------------------------------
1000:       0,7 µs        0,2 µs        0,2 µs        6,8 µs       0,2 µs
10000:       8,0 µs        1,4 µs        1,2 µs        7,8 µs       0,9 µs
100000:      72,4 µs       12,4 µs        8,2 µs       33,6 µs       7,5 µs
1000000:     652,9 µs      135,8 µs      101,6 µs      197,7 µs      71,6 µs
10000000:    7182,6 µs     4174,9 µs     5193,3 µs     3691,5 µs    1658,1 µs
100000000:   67142,3 µs    44853,3 µs    51372,5 µs    35195,5 µs   16585,1 µs

如果这段代码的性能不够,那么一个有希望的途径将是并行线性复制循环(所有线程使用相同的源块),或者我们的老朋友P/Invoke。

注意:块的清除和填充通常由运行时例程完成,该例程使用MMX/SSE指令和诸如此类的指令分支到高度专一的代码,因此在任何体面的环境中,人们只需调用各自的道德等等物std::memset,并确保专业的性能水平。低,按理说,标准库函数Array.Clear应该把我们所有的手卷版本都甩在后面。事实恰恰相反,这显示出情况有多么不正常。同样,首先必须滚动自己的Fill<>,因为它仍然只在核心和标准中,而不是在框架中。net已经存在了将近20年,我们仍然必须为最基本的东西左右P/调用或滚动我们自己的…

只是一个基准:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.997 (1909/November2018Update/19H2)
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.302
[Host]        : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT


Job=.NET Core 3.1  Runtime=.NET Core 3.1


|           Method |     Mean |     Error |    StdDev |
|----------------- |---------:|----------:|----------:|
| EnumerableRepeat | 2.311 us | 0.0228 us | 0.0213 us |
|  NewArrayForEach | 2.007 us | 0.0392 us | 0.0348 us |
|        ArrayFill | 2.426 us | 0.0103 us | 0.0092 us |
    [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.NetCoreApp31)]
public class InitializeArrayBenchmark {
const int ArrayLength = 1600;


[Benchmark]
public double[] EnumerableRepeat() {
return Enumerable.Repeat(double.PositiveInfinity, ArrayLength).ToArray();
}


[Benchmark]
public double[] NewArrayForEach() {
var array = new double[ArrayLength];


for (int i = 0; i < array.Length; i++) {
array[i] = double.PositiveInfinity;
}


return array;
}


[Benchmark]
public double[] ArrayFill() {
var array = new double[ArrayLength];
Array.Fill(array, double.PositiveInfinity);


return array;
}
}

我有点惊讶没有人做了非常简单,但超快的SIMD版本:

  public static void PopulateSimd<T>(T[] array, T value) where T : struct
{
var vector = new Vector<T>(value);
var i = 0;
var s = Vector<T>.Count;
var l = array.Length & ~(s-1);
for (; i < l; i += s) vector.CopyTo(array, i);
for (; i < array.Length; i++) array[i] = value;
}

基准测试:(数据来自于Framework 4.8,但Core3.1在统计上是相同的)

|     Method |       N |           Mean |          Error |        StdDev | Ratio | RatioSD |
|----------- |-------- |---------------:|---------------:|--------------:|------:|--------:|
| DarthGizka |      10 |      25.975 ns |      1.2430 ns |     0.1924 ns |  1.00 |    0.00 |
|       Simd |      10 |       3.438 ns |      0.4427 ns |     0.0685 ns |  0.13 |    0.00 |
|            |         |                |                |               |       |         |
| DarthGizka |     100 |      81.155 ns |      3.8287 ns |     0.2099 ns |  1.00 |    0.00 |
|       Simd |     100 |      12.178 ns |      0.4547 ns |     0.0704 ns |  0.15 |    0.00 |
|            |         |                |                |               |       |         |
| DarthGizka |    1000 |     201.138 ns |      8.9769 ns |     1.3892 ns |  1.00 |    0.00 |
|       Simd |    1000 |     100.397 ns |      4.0965 ns |     0.6339 ns |  0.50 |    0.00 |
|            |         |                |                |               |       |         |
| DarthGizka |   10000 |   1,292.660 ns |     38.4965 ns |     5.9574 ns |  1.00 |    0.00 |
|       Simd |   10000 |   1,272.819 ns |     68.5148 ns |    10.6027 ns |  0.98 |    0.01 |
|            |         |                |                |               |       |         |
| DarthGizka |  100000 |  16,156.106 ns |    366.1133 ns |    56.6564 ns |  1.00 |    0.00 |
|       Simd |  100000 |  17,627.879 ns |  1,589.7423 ns |   246.0144 ns |  1.09 |    0.02 |
|            |         |                |                |               |       |         |
| DarthGizka | 1000000 | 176,625.870 ns | 32,235.9957 ns | 1,766.9637 ns |  1.00 |    0.00 |
|       Simd | 1000000 | 186,812.920 ns | 18,069.1517 ns | 2,796.2212 ns |  1.07 |    0.01 |

可以看到,它在10000个元素时要快得多,超过10000个元素时只会稍微慢一点。

. net Core 2.0及以后版本支持Array.Fill()方法。

下面是一个示例代码。

var arr = new int[10];
int defaultValue = 2;
Array.Fill(arr,defaultValue);

对于要填充的索引范围,它还具有重载方法。更多细节可以找到在这里