今天在編碼的時候遇到了一個問題,需要對數組變量添加新元素和刪除元素,因為數組是固定大小的,因此對新增和刪除并不友好,但有時候又會用到,因此想針對數組封裝兩個擴展方法:新增元素與刪除元素,并能到達以下三個目標:
1、性能優異;
2、兼容性好;
3、方便使用;
這三個目標最麻煩的應該就是性能優異了,比較后面兩個可以通過泛型方法,擴展方法,按引用傳遞等語法實現,性能優異卻要在十來種實現方法中選出兩個最優的實現。那關于數組新增和刪除元素你能想到多少種實現呢?下面我們來一起看看那個性能最好。
01、新增元素實現方法對比
1、通過List方法實現
通過轉為List,再用AddRange方法添加元素,最后再轉為數組返回。代碼實現如下:
public static int[] AddByList(int[] source, int[] added)
{
var list = source.ToList();
list.AddRange(added);
return list.ToArray();
}
2、通過IEnumerable方法實現
因為數組實現了IEnumerable接口,所以可以直接調用Concat方法實現兩個數組拼接。代碼實現如下:
public static int[] AddByConcat(int[] source, int[] added)
{
return source.Concat(added).ToArray();
}
3、通過Array方法實現
Array有個Copy靜態方法可以實現把數組復制到目標數組中,因此我們可以先構建一個大數組,然后用Copy方法把兩個數組都復制到大數組中。代碼實現如下:
public static int[] AddByCopy(int[] source, int[] added)
{
var size = source.Length + added.Length;
var array = new int[size];
Array.Copy(source, array, source.Length);
Array.Copy(added, 0, array, source.Length, added.Length);
return array;
}
4、通過Span方法實現
Span也有一個類似Array的Copy方法,功能也類似,就是CopyTo方法。代碼實現如下:
public static int[] AddBySpan(int[] source, int[] added)
{
Span<int> sourceSpan = source;
Span<int> addedSpan = added;
Span<int> span = new int[source.Length + added.Length];
sourceSpan.CopyTo(span);
addedSpan.CopyTo(span.Slice(sourceSpan.Length));
return span.ToArray();
}
我想到了4種方法來實現,如果你有不同的方法希望可以給我留言,不吝賜教。那么那種方法效率最高呢?按我理解作為現在.net core性能中的一等公民Span應該性能是最好的。
我們也不瞎猜了,直接來一組基準測試對比。我們對4個方法,分三組測試,每組分別隨機生成兩個100、1000、10000個元素的數組,然后每組再進行10000次測試。
測試結果如下:
整體排名:AddByCopy > AddByConcat > AddBySpan > AddByList。
可以發現性能最好的竟然是Array的Copy方法,不但速度最優,而且內存使用方面也是最優的。
而我認為性能最好的Span整體表現還不如IEnumerable的Concat方法。
最終Array的Copy方法完勝。
02、刪除元素實現方法對比
1、通過List方法實現
還是先把數組轉為List,然后再用RemoveAll進行刪除,最后把結果轉為數組返回。代碼實現如下:
public static int[] RemoveByList(int[] source, int[] added)
{
var list = source.ToList();
list.RemoveAll(x => added.Contains(x));
return list.ToArray();
}
2、通過IEnumerable方法實現
因為數組實現了IEnumerable接口,所以可以直接調用Where方法進行過濾。代碼實現如下:
public static int[] RemoveByWhere(int[] source, int[] added)
{
return source.Where(x => !added.Contains(x)).ToArray();
}
3、通過Array方法實現
Array有個FindAll靜態方法可以實現根據條件查找數組。代碼實現如下:
public static int[] RemoveByArray(int[] source, int[] added)
{
return Array.FindAll(source, x => !added.Contains(x));
}
4、通過For+List方式實現
直接遍歷原數組,把滿足條件的元素放入List中,然后轉為數組返回。代碼實現如下:
public static int[] RemoveByForList(int[] source, int[] added)
{
var list = new List<int>();
foreach (int item in source)
{
if (!added.Contains(item))
{
list.Add(item);
}
}
return list.ToArray();
}
5、通過For+標記+Copy方式實現
還是直接遍歷原數組,但是我們不創建新集合,直接把滿足的元素放在原數組中,因為從原數組第一個元素迭代,如果元素滿足則放入第一個元素其索引自動加1,如果不滿足則等下一個滿足的元素放入其索引保持不變,以此類推,直至所有元素處理完成,最后再把原數組中滿足要求的數組復制到新數據中返回。代碼實現如下:
public static int[] RemoveByForMarkCopy(int[] source, int[] added)
{
var idx = 0;
foreach (var item in source)
{
if (!added.Contains(item))
{
source[idx++] = item;
}
}
var array = new int[idx];
Array.Copy(source, array, idx);
return array;
}
6、通過For+標記+Resize方式實現
這個方法和上一個方法實現基本一致,主要差別在最后一步,這個方法是直接通過Array的Resize靜態方法把原數組調整為我們要的并返回。代碼實現如下:
public static int[] RemoveByForMarkResize(int[] source, int[] added)
{
var idx = 0;
foreach (var item in source)
{
if (!added.Contains(item))
{
source[idx++] = item;
}
}
Array.Resize(ref source, idx);
return source;
}
同樣的我們再做一組基準測試對比,結果如下:
可以發現最后兩個方法隨著數組元素增加性能越來越差,而其他四種方法相差不大。既然如此我們就選擇Array原生方法FindAll。
03、實現封裝方法
新增刪除的兩個方法已經確定,我們第一個目標就解決了。
既然要封裝為公共的方法,那么就必要要有良好的兼容性,我們示例雖然都是用的int類型數組,但是實際使用中不知道會碰到什么類型,因此最好方式是選擇泛型方法。這樣第二個目標就解決了。
那么第三個目標方便使用要怎么辦呢?第一想法既然做成公共方法了,直接做一個幫助類,比如ArrayHelper,然后把兩個實現方法直接以靜態方法放進去。
但是我更偏向使用擴展方法,原因有二,其一可以利用編輯器直接智能提示出該方法,其二代碼更簡潔。形如下面兩種形式,你更喜歡那種?
var result = source.Add(added);
var result = ArrayHelper.Add(source, added);
現在還有一個問題,這個方法是以返回值的方式返回最后的結果呢?還是直接修改原數組呢?兩種方式各有優點,返回新數組,則原數組不變便于鏈式調用也避免一些副作用,直接修改原數組內存效率高。
我們的兩個方法是新增元素和刪除元素,其語義更貼合對原始數據進行操作其結果也作用在自身。因此我更傾向無返回值的方式。
那現在有個尷尬的問題,不知道你還記得我們上一章節《C#|.net core 基礎 - 值傳遞 vs 引用傳遞》講的值傳遞和引用傳遞,這里就有個這樣的問題,如果我們現在想用擴展方法并且無返回值直接修改原數組,那么需要對擴展方法第一個參數使用ref修飾符,但是擴展方法對此有限制要求【第一個參數必須是struct 或是被約束為結構的泛型類型】,顯示泛型數組不滿足這個限制。因此無法做到我心目中最理想的封裝方式了,下面看看擴展方法和幫助類的代碼實現,可以按需使用吧。
public static class ArrayExtensions
{
public static T[] AddRange<T>(this T[] source, T[] added)
{
var size = source.Length + added.Length;
var array = new T[size];
Array.Copy(source, array, source.Length);
Array.Copy(added, 0, array, source.Length, added.Length);
return array;
}
public static T[] RemoveAll<T>(this T[] source, Predicate<T> match)
{
return Array.FindAll(source, a => !match(a));
}
}
public static class ArrayHelper
{
public static void AddRange<T>(ref T[] source, T[] added)
{
var size = source.Length + added.Length;
var array = new T[size];
Array.Copy(source, array, source.Length);
Array.Copy(added, 0, array, source.Length, added.Length);
source = array;
}
public static void RemoveAll<T>(ref T[] source, Predicate<T> match)
{
source = Array.FindAll(source, a => !match(a));
}
}
注:測試方法代碼以及示例源碼都已經上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner
?轉自https://www.cnblogs.com/hugogoos/p/18421745
該文章在 2025/1/6 10:07:20 編輯過