C#|.net core 基礎 - 擴充套件陣列新增刪除效能最好的方法

IT规划师發表於2024-09-20

今天在編碼的時候遇到了一個問題,需要對陣列變數新增新元素和刪除元素,因為陣列是固定大小的,因此對新增和刪除並不友好,但有時候又會用到,因此想針對陣列封裝兩個擴充套件方法:新增元素與刪除元素,並能到達以下三個目標:

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

相關文章