C#|.net core 基础 - 扩展数组添加删除性能最好的方法

今天在编码的时候遇到了一个问题,需要对数组变量添加新元素和删除元素,因为数组是固定大小的,因此对新增和删除并不友好,但有时候又会用到,因此想针对数组封装两个扩展方法:新增元素与删除元素,并能到达以下三个目标:
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
C#|.net core 基础 - 扩展数组添加删除性能最好的方法的更多相关文章
- JS数组添加删除
栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处.原文: https://www.w3cplus.com/j ...
- Entity Framework入门教程(12)--- EF进行批量添加/删除
EF6添加了批量添加/删除实体集合的方法,我们可以使用DbSet.AddRange()方法将实体集合添加到上下文,同时实体集合中的每一个实体的状态都标记为Added,在执行SaveChange()方法 ...
- ES6数组中删除指定元素
知识点: ES6从数组中删除指定元素 findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引.否则返回-1. arr.splice(arr.findIndex(item => ...
- JavaScript Dom基础-9-Dom查找方法; 设置DOM元素的样式; innerHTML属性的应用; className属性的应用; DOM元素上添加删除获取属性;
JavaScript Dom基础 学习目标 1.掌握基本的Dom查找方法 domcument.getElementById() Domcument.getElementBy TagName() 2.掌 ...
- jquery中找到元素在数组中位置,添加或者删除元素的新方法
一:查找元素在数组中的位置 jQuery.inArray()函数用于在数组中搜索指定的值,并返回其索引值.如果数组中不存在该值,则返回 -1. jQuery.inArray( value, array ...
- JavaScript中的内置对象-8--1.Array(数组)-Array构造函数; 数组的栈方法; 数组的转换方法; 数组的操作方法; 删除-插入-替换数组项; ECMAScript为数组实例添加的两个位置方法;
JavaScript内置对象-1Array(数组) 学习目标 1.掌握任何创建数组 2.掌握数值元素的读和写 3.掌握数组的length属性 如何创建数组 创建数组的基本方式有两种: 1.使用Arra ...
- js数组添加或删除元素
var arr = new Array(); arr[] = "aaa"; arr[] = "bbb"; arr[] = "ccc"; ar ...
- JavaScript学习 - 基础(八) - DOM 节点 添加/删除/修改/属性值操作
html代码: <!--添加/删除/修改 --> <div id="a1"> <button id="a2" onclick=&q ...
- PHP:第四章——PHP数组添加,删除,插入,分割,合并,及运算符
<pre> <?php header("Content-Type:text/html;charset=utf-8"); /*知识点一:赋值运算符 = 代码示例:数 ...
- ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 新增用户 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 新增用户 上一章节我们实现了一个注册表单,但也留了一些东西还没完成, ...
随机推荐
- oeasy教您玩转vim - 74 - # 功能键Fn映射map进阶
映射map 回忆上次缩写的细节 上次了解到了:map映射 可以定义映射 :map ; : 查询映射 :map :map ; 还可以取消映射 :unmap 可以映射到指定的模式 :map 所有模式 ...
- [rCore学习笔记 014]批处理系统
写在前面 本随笔是非常菜的菜鸡写的.如有问题请及时提出. 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 本章目 ...
- Jmeter让线程循环变量值不重复
我们定义用户参数时为了保证某个参数值不重复会设置为随机变量 1.使用[用户定义的变量]组件,传入随机值如"HELLO${__Random(100,200,)}_${__counter(FAL ...
- Jmeter函数助手29-dateTimeConvert
dateTimeConvert函数用于将源格式进行目标格式的转换. 格式化时间:传入时间参数,此处格式需要与源时间格式一致 源时间格式:传入参数的时间格式 目标时间格式:想要转换成的格式 1.将源格式 ...
- 【Vue】Re19 Promise
一.概述 Promise是异步编程的解决方案 异步事件的处理: 封装的异步请求函数不能立即获取结果, 通常会传入另外一个函数,在请求成功的时候将数据通过传入的函数回调出去 如果只是一个简单的请求,那么 ...
- “Vanilla” 在计算机科学和技术领域中的专业翻译
"Vanilla" 在计算机科学和技术领域中通常指的是某个系统或软件的"原始"或"基础"版本,即没有任何修改.扩展或定制的版本.它可以翻译为 ...
- 如何在python同一应用下的多模块中共享变量
最近在考虑编码风格的问题,突然想到如何在一个python应用下的多个模块中共享一个变量.最早接触python还是在python2.5版本左右,那个时候由于python的import规则设定的问题导致本 ...
- 对于围棋AI作弊的一些思考
最近看到些关于围棋AI作弊的报道有了一些思考. 相关视频链接: https://www.bilibili.com/video/BV1np411f73b/?spm_id_from=autoNext ht ...
- [SDOI2010] 城市规划 题解
前言 题目链接:洛谷. 题意简述 树套环上求至少间隔两个位置的最大独立集. (树套环,即树上每个结点都是一个结点或环) 题目分析 将题目拆解成树上 DP 和环上 DP 即可.用 tarjan 缩点就行 ...
- JavaScript设计模式样例七 —— 原型模式
原型模式(Prototype Pattern) 定义:用于创建重复的对象,同时又能保证性能.目的:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.场景:在运行期建立和删除原型. let ...