一、集合
集合重要且常用
孤立的数据是没有意义的,集合可以作为大量数据的处理,可进行数据的搜索、迭代、添加、删除。
C#中,所有集合都必须实现ICollection接口(数组Array除外)
集合 | 说明 |
---|---|
Array | 数组,固定长度、固定类型 |
ArrayList | 列表,可变长度、任意类型 |
List<T> | 列表,可变长度、固定类型 |
Dictionary<T> | 字典,键值对结构 |
Queue<T> | 队列,先进先出(FIFO)集合 |
Stack<T> | 栈,后进先出(LIFO)集合 |
IEnumerable<T> | 可迭代集合 |
C#集合的特点
- [ 可以储存无限个元素(除了数组) ]
- [ 任何一个集合都支持搜索、排序、复制、添加 、删除等操作 ]
二、数组
特点
1.固定长度
2.有明确顺序
使用数组是十分安全的,不会返回任何不存在的数据
例如:
class Program
{
static void Main(string[] args)
{
string[] daysOfWeek =
{
"Monday",
"Tuesday",
"Wednesday",
"Thuresday",
"Friday",
"Saturday",
"Sunday"
};
foreach (string day in daysOfWeek)
{
Console.WriteLine(day);
}
//零索引 0-indexed
Console.WriteLine(daysOfWeek[0]);//周一
Console.WriteLine(daysOfWeek[1]);//周二
//固定长度
string[] monthsOfYear = new string[12];
monthsOfYear[0] = "January";
monthsOfYear[1] = "February";
monthsOfYear[2] = "March";
monthsOfYear[3] = "April";
monthsOfYear[4] = "May";
monthsOfYear[5] = "June";
monthsOfYear[6] = "July";
monthsOfYear[7] = "August";
monthsOfYear[8] = "September";
monthsOfYear[9] = "October";
monthsOfYear[10] = "November";
monthsOfYear[11] = "December";
Console.Read();
}
}
三、列表与数组列表
1.List
在底层实现中,list依然使用数组承载数据,不过在数组装满数据以后,list会立刻创建新的数组来代替旧的数组,并且把所有数据复制装载到新的数组中,因此列表又可以成为动态数组。列表的容量不仅可以动态调整,也可以手动调整,对系统的动态调优取得很大帮助。
数组的访问速度略高于列表,但是列表对于空间的利用优于数组。
//List 列表
List<string> daysOfWeek2 = new List<string>();
daysOfWeek2.Add("Monday");
daysOfWeek2.Add("Tuesday");
daysOfWeek2.Add("Wednesday");
daysOfWeek2.Add("Thuresday");
daysOfWeek2.Add("Friday");
daysOfWeek2.Add("Saturday");
daysOfWeek2.Add("Sunday");
//只需要在前面加上I就可以声明接口的列表
IList<string> daysOfWeek3 = new List<string>();
2.ArrayList
List支持泛型,ArrayList不支持泛型,仅能保存对象
ArrayList装载数据很方便,但是提取数据较为麻烦,需要进行拆箱,影响性能
//ArrayList
var array = new ArrayList();
array.Add(daysOfWeek);
array.Add("123");
array.Add(1);
3.List的基本操作
List的有参数构造器
//List的有参数构造器
var daysOfWeek4 = new List<string>(daysOfWeek);//数组
var daysOfWeek5 = new List<string>(daysOfWeek2);//列表
var daysOfWeek6 = new List<string>(7);
List<string> daysOfWeek7 = new List<string>
{
"Monday",
"Tuesday",
"Wednesday",
"Thuresday",
"Friday",
"Saturday",
"Sunday"
};
列表的插入数据,可以使用Insert或者InsertRange
//列表插入,Insert,InsertRange
daysOfWeek7.InsertRange(2, daysOfWeek);//将daysOfWeek插入到第二个位置后
如果要将列表7插入到列表6的最前面
daysOfWeek6.InsertRange(0, daysOfWeek7);
但是更推荐
daysOfWeek7.AddRange(daysOfWeek6);
因为使用Insert操作,会自动将原来的列表分成两个列表,在进行插入操作,影响性能。
删除数据
//删除数据,RemoveAt, RemoveRange
daysOfWeek7.RemoveAt(0);
daysOfWeek7.RemoveRange(2, 6);
daysOfWeek7.Remove("Monday"); //只删除遍历到的第一个数据
daysOfWeek7.RemoveAll(i => i == "Monday");//删除遍历到的所有的Monday
daysOfWeek7.RemoveAll(i => i.Contains("day"));//删除所有包含day的数据
四、迭代器Enumerator与循环遍历ForEach
读取列表
//读取列表
var a = daysOfWeek6.Count;//读取数据个数
var b = daysOfWeek6.Capacity;//读取列表容量
索引器,方括号就是索引器,准确查找位置
//索引器,方括号就是索引器,准确查找位置
var c = daysOfWeek6[3];
迭代器,将集合按照一定规律全部访问一遍
var enumerator = daysOfWeek6.GetEnumerator();
var d = enumerator.Current;//迭代器当前所指的元素(此时为空)
enumerator.MoveNext();//此时指向第一个元素,遍历完成返回TRUE
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
//使用while循环,会在最后输出一个null,是由于Current悬空
}
//为了解决上面问题,所有可以使用foreach
foreach ( var day in daysOfWeek6)
{
Console.WriteLine(day);
}
Foreach遍历时不允许容量发生变化,所有元素均为只读数据,不允许修改
但是我们可以举个例子
Customers.cs
public class Customer
{
public Customer(int id, string name, string address)
{
Id = id;
Name = name;
Address = address;
}
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
Program.cs
List<Customer> customers = new List<Customer>();
customers.Add(new Customer(1, "小赵", "广州"));
customers.Add(new Customer(2, "小钱", "北京"));
customers.Add(new Customer(3, "小王", "上海"));
customers.Add(new Customer(4, "小孙", "深圳"));
foreach(var customer in customers)
{
customer.Name = "123";
Console.WriteLine(customer.Name);
}
此时,使用foreach却可以修改数据,为什么?
原因很简单,这里的customer是引用类型的数据,对于foreach,引用类型的数据本身地址是不改变的,因此这里的数据就可以完成修改。
IEumerable<T>
与IEnumerator<T>
**由于不带泛型需要涉及到装箱拆箱,因此以下只讨论带泛型的版本 *
创建Bank类,使用迭代器遍历列表
Bank.cs
public class Bank : IEnumerable<Customer>
{
public List<Customer> Customers { get; set; } = new List<Customer>();
public Bank()
{
Customers.Add(new Customer(1, "小赵", "广州"));
Customers.Add(new Customer(2, "小钱", "北京"));
Customers.Add(new Customer(3, "小王", "上海"));
Customers.Add(new Customer(4, "小孙", "深圳"));
}
public IEnumerator<Customer> GetEnumerator()
{
return Customers.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
Program.cs
var bank = new Bank();
foreach (var item in bank)
{
Console.WriteLine(item.Name);
}
下面,我们尝试自己创建一个迭代器,实现对于列表的遍历
创建MyEnumerator.cs
public class MyEnumerator<T> : IEnumerator<T>
{
T[] _data;
int _position = -1;//开始时,要让current悬空
public MyEnumerator(T[] data)
{
_data = data;
}
public T Current { get => _data[_position]; }
object IEnumerator.Current { get => Current; }
public void Dispose()
{
}
public bool MoveNext()
{
_position++;
return _position < _data.Length;
}
public void Reset()
{
_position = -1;
}
}
MyList.cs
public class MyList<T> : IEnumerable<T>
{
private T[] _data;
int cuttentIndex;
public MyList(int length)
{
this._data = new T[length];
cuttentIndex = 0;
}
public void Add(T item)
{
_data[cuttentIndex] = item;
cuttentIndex++;
}
public IEnumerator<T> GetEnumerator()
{
return new MyEnumerator<T>(_data);
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
Bank.cs
public class Bank : IEnumerable<Customer>
{
public MyList<Customer> Customers { get; set; } = new MyList<Customer>(4);
public Bank()
{
Customers.Add(new Customer(1, "小赵", "广州"));
Customers.Add(new Customer(2, "小钱", "北京"));
Customers.Add(new Customer(3, "小王", "上海"));
Customers.Add(new Customer(4, "小孙", "深圳"));
}
public IEnumerator<Customer> GetEnumerator()
{
return Customers.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
五、迭代与yield return
示例一个场景,有一百万个用户数据,我们需要调取前1000个客户数据
static void Main(string[] args)
{
var customers = GetCustomers(1000000);
foreach (var customer in customers)
{
if(customer.Id < 1000)
{
Console.WriteLine($"客户Id{customer.Id}, 客户姓名:{customer.Name}");
}
else
{
break;
}
}
}
static IEnumerable<Customer> GetCustomers(int count)
{
var customers = new List<Customer>();
for (int i = 0; i < count; i++)
{
customers.Add(new Customer(i, $"Crackpot{i}", "天津"));
}
return customers;
}
此时,创建了1000000个数据,但是实际上只需要前1000个数据,对于其余的数据,内存空间完全浪费了,因为根本不会用到
因此,使用yield关键词,就可以实现一个懒加载的效果;yield语句的执行次数与if语句执行次数相同,可以极大地提升内存利用率。yield语句并不返回数据,而是返回数据的迭代。
static void Main(string[] args)
{
var customers = GetCustomersYield(1000000);;
foreach (var customer in customers)
{
if(customer.Id < 1000)
{
Console.WriteLine($"客户Id{customer.Id}, 客户姓名:{customer.Name}");
}
else
{
break;
}
}
}
static IEnumerable<Customer> GetCustomersYield(int count)
{
var customers = new List<Customer>();
for (int i = 0; i < count; i++)
{
yield return new Customer(i, $"Crackpot{i}", "天津");
举例2
static void Main(string[] args)
{
foreach (var i in Createnumerable())
{
Console.WriteLine(i);
}
}
static IEnumerable<int> Createnumerable()
{
yield return 3;
yield return 2;
yield return 1;
}
输出结果为3 2 1
,可以看到yield可以实现非必要,不创建的原则
六、Benchmark性能基准测试
首先安装NuGet包
我们使用Benchmark来查看两种方法使用不使用yield的差距
创建BenchmarkTester.cs
[MemoryDiagnoser]
public class BenchmarkTester
{
[Benchmark]
public void ProcessCustomer()
{
var customers = GetCustomers(1000000);
foreach (var customer in customers)
{
if (customer.Id < 1000)
{
Console.WriteLine($"客户id:{customer.Id}, 客户姓名:{customer.Name}");
}
else
{
break;
}
}
}
[Benchmark]
public void ProcessCustomerYield()
{
var customers = GetCustomersYield(1000000);
foreach (var customer in customers)
{
if (customer.Id < 1000)
{
Console.WriteLine($"客户id:{customer.Id}, 客户姓名:{customer.Name}");
}
else
{
break;
}
}
}
static IEnumerable<Customer> GetCustomersYield(int count)
{
var customers = new List<Customer>();
for (int i = 0; i < count; i++)
{
yield return new Customer(i, $"Crackpot{i}", "天津");
}
}
static IEnumerable<Customer> GetCustomers(int count)
{
var customers = new List<Customer>();
for (int i = 0; i < count; i++)
{
customers.Add(new Customer(i, $"Crackpot{i}", "天津"));
}
return customers;
}
}
然后再Program.cs中调用Benchmark进行测试var sumery = BenchmarkRunner.Run<BenchmarkTester>();
需要注意,需要在CMD中执行benchmark测试
找到项目文件,然后dotnet build -c Release
会生成文件于:项目名->bin->Release->net8.0->项目名.dll
dotnet 项目.dll
出现结果
可以看到,使用yield的运行时间是不使用的大约1/2,而使用的内存分配仅为约1/671,足以看到yield对于系统运行性能的提升
七、数据搜索:字典
使用示例
program.cs
static void Main(string[] args)
{
var customers = GetCustomerDictionary(1000000);
customers.GetValueOrDefault(999999);
var customer = customers[999999];
Console.WriteLine($"客户id:{customer.Id}, 客户姓名:{customer.Name}");
}
static Dictionary<int, Customer> GetCustomerDictionary(int count)
{
var customer = new Dictionary<int, Customer>();
for (int i = 0; i < count; i++)
{
customer.Add(i, new Customer(i, $"Crackpot{i}", "广州"));
}
return customer;
}
八、哈希表
C#中哈希表与字典几乎没有泛型,最显著区别是哈希表没有泛型而字典有泛型
哈希表的值均为object类型,因此难免会使用装箱或拆箱,因此非常耗时
static void Main(string[] args)
{
var customerHashTable = GetCustomerHashTable(1000000);
var customer = (Customer)customerHashTable[99999];
Console.WriteLine($"客户id:{customer.Id}, 客户姓名:{customer.Name}");
}
static Hashtable GetCustomerHashTable(int count)
{
var customer = new Hashtable();
for (int i = 0; i < count; i++)
{
customer.Add(i, new Customer(i, $"Crackpot{i}", "广州"));
}
return customer;
}
九、集合的交、并、差运算(HashSet)
HashSet并不常用,但是在处理一些特殊问题时,非常便捷。
示例为:查找公交线路系统
Program.cs
static void Main(string[] args)
{
var database = new BusRouteRepository();
Console.WriteLine("从哪里来?");
string startingAt = Console.ReadLine();
Console.WriteLine("到哪里去?");
string goingTo = Console.ReadLine();
var startingRoutes = database.FindBusTo(startingAt);
var destination = database.FindBusTo(goingTo);
HashSet<BusRoute> routes = new HashSet<BusRoute>(startingRoutes);
routes.IntersectWith(destination);
if(routes.Count > 0)
{
foreach(var route in routes)
{
Console.WriteLine($"乘坐公交车:{route}");
}
}
else
{
Console.WriteLine("路线找不到");
}
Console.Read();
}
BusRoute.cs
public class BusRoute
{
public int Number { get; }
public string Origin => PlacesServed[0];
public string Destination => PlacesServed[^1];
public string[] PlacesServed { get; }
public BusRoute(int number, string[] placesServed)
{
this.Number = number;
this.PlacesServed = placesServed;
}
public override string ToString() => $"{Number}: {Origin} -> {Destination}";
public bool Serves(string destination)
{
return Array.Exists(PlacesServed, place => place == destination);
}
}
BusRouteRepository.cs
public class BusRouteRepository
{
private readonly BusRoute[] _allRoutes;
public BusRouteRepository()
{
_allRoutes = new BusRoute[]
{
new BusRoute(101, new string[] {"火车站","大学城","动物园","体育馆"}),
new BusRoute(42, new string[] {"火车站","电子城","花园酒店","体育馆"}),
new BusRoute(232, new string[] {"理工大学","海洋馆","购物中心","游泳馆","体育馆"}),
new BusRoute(51, new string[] {"美食广场","长途汽车站","游乐园","机场"}),
new BusRoute(6, new string[] {"南井村","双河营村","长途汽车站","火车站"}),
};
}
public BusRoute[] FindBusTo(string location)
{
return Array.FindAll(_allRoutes, route => route.Serves(location));
}
public BusRoute[] FindBusesBetween(string location1, string location2)
{
return Array.FindAll(_allRoutes, route => route.Serves(location1) && route.Serves(location2));
}
}
示例结果