第壹章第6节 C#和TS语言对比-数组集合

本节比较长,但可以让我们一次性彻底掌握数组集合。编程的很大一部分工作,是访问和操作数据,而其中使用频率最高的数据形式,就是数组集合。C#毕竟是后端语言,所以提供了更加丰富的集合类型,而且还有非常非常非常强大的LINQ查询语法,而TS就相对简陋些,毕竟是前端语言。文末补充了元组,这玩意我很少用,具体原因详见正文。

一、C#的数组和集合

C#中的数组和集合,有相似性,但本质上是不同的,主要表现为:(1)两者内存使用方式不一样。数组的类型和长度在定义时就已经确定,不可变,所以在内存中是一个固定长度的连续内存块。而集合即使初始化,它也是动态可变的,在内存中不是连续的,当分配的内存使用完后,框架会根据特定算法动态增加一段内存。(2)数组定义时类型和长度都确定了,通常性能更好,同时它还有多维、交错等数据形式;集合的长度是动态的,类型也有泛型形式,灵活性强,使用频率大,但是它的性能相比数组会差些,也只有一维形式。(3)根据经验,如果数据是固定长度且有数值运算,或者有多维或交错需求,可以使用数组,通过这种情况很少。

1.1 数组

1.1.1 数组的定义和初始化
//1、方式一:初始化器,最常用=========================================================
int[] numbers = { 1, 2, 3, 4, 5 }; // 一维数组
int[,] matrix = { { 1, 2 }, { 3, 4 } }; // 二维数组
int[][] jaggedArray = {new int[] { 1, 2 },new int[] { 3, 4, 5 }}; // 交错数组


//2、方式二:new关键词,最少用========================================================
int[] numbers = new int[5] { 1, 2, 3, 4, 5 }; //初始化一个长度为5的数组,并赋值
int[,] matrix = new int[2, 2] { { 1, 2 }, { 3, 4 } }; //初始化一个2x2的二维数组,并赋值
// 初始化一个长度为2的交错数组,并赋值
int[][] jaggedArray = new int[2][] {new int[] { 1, 2 },new int[] { 3, 4, 5 }}; 


//3、C#12新增的集合表达式创建数组=====================================================
int[] a = [1, 2, 3, 4, 5, 6, 7, 8]; //创建数组
List<string> b = ["one", "two", "three"]; //创建集合
Span<char> c  = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'i']; //创建span
int[][] twoD = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; //创建二维数组
//展开运算符
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [..row0, ..row1, ..row2]; //创建数组


//4、以默认值初始化数组长度===========================================================
int[] numbers = new int[5]; // 初始化一个长度为5的数组,所有元素默认值为0
int[,] matrix = new int[3, 3]; // 初始化一个3x3的二维数组,所有元素默认值为0
int[][] jaggedArray = new int[3][]; // 初始化一个交错数组

1.1.2 数组的访问和修改
//1、访问和修改数组元素===============================================================
int[] numbers = { 1, 2, 3, 4, 5 };
int firstNumber = numbers[0]; // 获取第一个元素,值为1
numbers[0] = 10; // 将第一个元素修改为10
matrix[1,1] = 1; //设置二维数组元素的值
jaggedArray[0] = new int[] { 12 }; //设置交错数组元素的值



//2、获取数组长度并遍历===============================================================
int[] numbers = { 1, 2, 3, 4, 5 };
int length = numbers.Length; // 获取数组长度
// 使用for循环遍历数组
for (int i = 0; i < length; i++)
{
    Console.WriteLine(numbers[i]);
}
// 使用foreach循环遍历数组
foreach (int number in numbers)
{
    Console.WriteLine(number);
}

1.1.3 数组常用API
//1、静态属性=======================================================================
Array.Empty<T>() //返回类型为T的空数组

    
//2、实例属性=======================================================================
Length //获取数组中元素的总数
LongLength //获取数组中元素的总数(返回long类型)
Rank //获取数组的维数(例如,二维数组的Rank为2)

    
//3、静态方法=======================================================================
Array.Clear(...) //将数组中指定范围的元素设置为默认值
Array.Copy(...) // 将一个数组的元素复制到另一个数组或其指定范围中,有多个重载方法
Array.CreateInstance(...) // 创建指定类型的一维或多维数组,有多个重载方法
Array.IndexOf(...) //返回一维数组或其指定范围中,指定值的第一个匹配项索引,有多个重载
Array.LastIndexOf(...) //同上,但返回最后一个匹配项
Array.Reverse(...) // 反转一维数组中所有元素或指定范围元素的顺序,有多个重载方法
Array.Sort(...) //对一维数组中的元素或指定范围元素进行排序,有多个重载方法
Array.BinarySearch(...) // 使用二分查找法搜索整个一维排序数组中的元素,有多个重载方法


//4、实例方法======================================================================
Clone() //创建当前数组的浅表副本。
CopyTo(Array array, int index) //将实例的所有元素复制到指定的一维数组的指定开始位置
GetLength(int dimension) //获取指定维度的长度。
GetLongLength(int dimension) //获取指定维度的长度(返回long类型)。
GetValue(int index) //获取指定索引位置的元素值
GetValue(int index1, int index2) //获取指定二维索引位置的元素值
GetValue(int index1, int index2, int index3) // 获取指定三维索引位置的元素值
GetValue(params int[] indices) // 获取指定多维索引位置的元素值
SetValue(object value, int index) // 设置指定索引位置的元素值
SetValue(object value, int index1, int index2) //设置指定二维索引位置的元素值
SetValue(object value, int index1, int index2, int index3) //设置指定三维索引位置元素值
SetValue(object value, params int[] indices) //设置指定多维索引位置的元素值

1.2 集合

1.2.1 集合大家族的关系

下图来自网络,C#的集合包括泛型和非泛型,非泛型存在装箱拆箱的问题,尽量不要使用,绝大多数情况下,应该使用泛型集合。大概说一下关系:

  • IEnumerator定义了遍历集合的方法-迭带器,IEnumerable只有一个方法-GetEnumerator,即获取迭带器
  • ICollection添加了一系列集合操作方法,比如添加/移除元素、统计/判断元素、获取元素下标等
  • 从ICollection派生出两类集合,并添加了各自的API。一是列表集合,通过下标来操作元素,如List、LinkedList、HashSet、SotedSet、Stack、Queue;二是字典集合(键值对),通过键来操作元素,如Dictionary、SortedDic、SortedList、ConcurrentDic等。
  • List和Dictionary<K,V>最常使用,其它使用比较少。

image.png

1.2.2 常用泛型列表集合
//1、List<T>=========================================================================
/*
动态数组,能够按索引访问
允许重复元素
保持元素的插入顺序
提供丰富的方法来操作集合,最常用
*/
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
numbers.Add(2); // 允许重复元素
foreach (int number in numbers)
{
    Console.WriteLine(number);
}
//List<T>集合的初始化
List<int> numbers1 = new List<int> { 1, 2, 3 };
List<int> numbers2 = new(){ 1, 2, 3 }; //省略类型申明,最常用
List<int> numbers3 = [1, 2, 3]; //C#12新增的集合表达式
//指定集合的容量
List<int> numbers = new List<int>(10);


//2、LinkedList<T>==================================================================
/*
双向链表,适合频繁插入和删除的操作
不支持按索引访问
允许重复元素
保持元素的插入顺序
*/
LinkedList<string> names = new LinkedList<string>();
names.AddLast("Alice");
names.AddLast("Bob");
names.AddLast("Charlie");


//3、HashSet<T>=====================================================================
/*
集合,不允许重复元素
基于哈希表实现,提供高效的添加、删除和查找操作
元素没有特定的顺序
*/
HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(3);
uniqueNumbers.Add(2); // 重复元素不会被添加


//4、SortedSet<T>===================================================================
/*
集合,不允许重复元素
元素按排序顺序存储
基于红黑树实现,提供高效的添加、删除和查找操作
*/
SortedSet<string> sortedNames = new SortedSet<string>();
sortedNames.Add("Bob");
sortedNames.Add("Alice");
sortedNames.Add("Charlie");


//5、Stack<T>=======================================================================
/*
后进先出(LIFO)集合
适合存储和访问需要按相反顺序处理的元素
提供Push、Pop和Peek方法
*/
Stack<string> stack = new Stack<string>();
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
while (stack.Count > 0)
{
    Console.WriteLine(stack.Pop()); // 输出顺序:Third, Second, First
}


//6、Queue<T>=======================================================================
/*
先进先出(FIFO)集合
适合按插入顺序处理元素的场景
提供Enqueue和Dequeue方法
*/
Queue<int> queue = new Queue<int>();
ueue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
while (queue.Count > 0)
{
    Console.WriteLine(queue.Dequeue()); // 输出顺序:1, 2, 3
}

1.2.3 常用泛型字典集合
//1、Dictionary<K, T>===============================================================
/*
基于哈希表实现的键值对集合
提供快速的查找、添加和删除操作
键必须是唯一的,值可以重复
元素没有特定的顺序
最常用
*/
Dictionary<int, string> dictionary = new Dictionary<int, string>();
dictionary.Add(3, "Three");
dictionary.Add(1, "One");
dictionary.Add(2, "Two");
foreach (var kvp in dictionary)
{
    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}"); //按3-1-2输出
}

Dictionary<int, string> dict = new Dictionary<int, string>(10); //指定容量


//2、SortedDictionary<K, T>=========================================================
/*
基于红黑树实现的键值对集合
键必须是唯一的,值可以重复
按键的排序顺序存储元素
提供有序的查找、添加和删除操作
*/
SortedDictionary<int, string> sortedDictionary = new SortedDictionary<int, string>();
sortedDictionary.Add(3, "Three");
sortedDictionary.Add(1, "One");
sortedDictionary.Add(2, "Two");
foreach (var kvp in sortedDictionary)
{
    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}"); //按1-2-3输出
}


//3、SortedList<K, T>===============================================================
/*
基于有序列表的键值对集合
键必须是唯一的,值可以重复
按键的排序顺序存储元素
提供高效的按索引访问和有序的查找操作,但在插入和删除操作时效率较低(因为需要维护有序性)
*/
SortedList<int, string> sortedList = new SortedList<int, string>();
sortedList.Add(3, "Three");
sortedList.Add(1, "One");
sortedList.Add(2, "Two");
foreach (var kvp in sortedList)
{
    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}"); //按1-2-3输出
}


//4、ConcurrentDictionary<K, T>=====================================================
/*
线程安全的键值对集合,适用于多线程环境
提供快速的并发读取和写入操作
键必须是唯一的,值可以重复
基于分段锁机制实现,以提高并发性能
没用过,很少手动去操作并发,了解一下
*/
ConcurrentDictionary<int, string> cd = new ConcurrentDictionary<int, string>();

// 并发写入操作
Parallel.For(0, 10, i =>
{
    cd.TryAdd(i, $"Value {i}");
});
// 并发读取操作
Parallel.ForEach(cd, kvp =>
{
    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
});

1.2.4 List常用API
//1、创建一个List====================================================================
List<int> numbers = new(){ 1, 2, 3, 4, 5 };


//2、集合容量和元素数量属性===========================================================
Console.WriteLine(numbers.Count); //集合元素数量,5
Console.WriteLine(numbers.Capacity); //集合容量,自动分配,8

numbers.Capacity = 10; //指定集合的容量
Console.WriteLine(numbers.Count); //输出5
Console.WriteLine(numbers.Capacity); //输出10

numbers.TrimExcess(); //将容量设置为元素数量,可以节省内存
Console.WriteLine(numbers.Count); //输出5
Console.WriteLine(numbers.Capacity); //输出5


//3、排序和反转======================================================================
//排序-----------------------------
numbers.Sort(); //简单排序
numbers.Sort(2, 4, Comparer<int>.Default); //指定索引2-4,使用默认比较器排序
List<string> names = new List<string> { "Charlie", "Alice", "Bob" };
names.Sort((x, y) => x.Length.CompareTo(y.Length)); //使用自定义比较器按字符串长度排序
//反转----------------------------
numbers.Reverse(); //简单反转
numbers.Reverse(2, 4); //反转索引2到4的元素


//4、添加元素========================================================================
numbers.Add(6); //插入一个元素到集合最后
numbers.AddRange(new int[] { 7, 8, 9 }); //插入多个元素到集合最后
numbers.Insert(0, 0); //在指定位置插入一个元素
numbers.InsertRange(10, new int[] { 10, 11 }); //在指定位置插入多个元素


//5、删除元素========================================================================
numbers.Remove(42);
numbers.RemoveAt(5);
numbers.RemoveRange(0, 3);


//6、访问和修改元素===================================================================
Console.WriteLine(numbers[5]); // 输出: 4
numbers[5] = 42;


//7、遍历操作========================================================================
numbers.ForEach(n => Console.WriteLine(n)); 
foreach (var number in numbers)
{
    Console.WriteLine(number);
}


//8、复制操作========================================================================
int[] array = numbers.ToArray(); //将元素复制到一个新数组
numbers.CopyTo(array); //将元素复制到一个一维数组


//9、查找元素========================================================================
//9.1 判断元素是否存在----------------------
bool contains = numbers.Contains(42); //判断某元素是否在集合中
bool contains = numbers.Exists(n => n>10); //是否存在大于10的数

//9.2 查找索引值-----------------------------
int index = numbers.IndexOf(42); //返回第一个匹配元素的索引值
int lastIndex = numbers.LastIndexOf(42); //返回最后一个匹配元素的索引值
int index = numbers.FindIndex(x => x > 25); //查找第一个大于25的元素的索引
int index = numbers.FindIndex(2, x => x > 25); //从索引2开始查找
int index = numbers.FindIndex(2, 3, x => x > 45); //从索引2开始在接下来3个元素中
int index = numbers.FindLastIndex(x => x > 25); //查找最后一个匹配索引,也有3个重载方法

//9.3 查找元素-----------------------------
int match = numbers.Find(x => x > 25); //查找第一个匹配元素,如找不到返回T默认值
int match = numbers.FindLast(x => x > 25); //查找最后一个匹配元素
List<int> matchs = numbers.FindAll(x => x > 25); //查找所有匹配的元素

1.2.5 Dictionary<K,V>常用API
//1、使用集合初始化器初始化==========================================================
Dictionary<int, string> dict = new Dictionary<int, string>
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" }
};


//2、获取和设置值====================================================================
string value = dict[1]; //获取值, "One"
dict[1] = "Uno"; //设置值


//3、尝试获取值======================================================================
if (dict.TryGetValue(3, out string result))
{
    Console.WriteLine(result); // "Three"
}


//4、检查键和值======================================================================
bool hasKey = dict.ContainsKey(2); // true
bool hasValue = dict.ContainsValue("Three"); // true


//5、获取键和值集合==================================================================
ICollection<int> keys = dict.Keys;
ICollection<string> values = dict.Values;


//6、循环遍历字典====================================================================
foreach (KeyValuePair<int, string> kvp in dict)
{
    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}


//7、移除键值对=====================================================================
dict.Remove(2);


//8、清空字典=======================================================================
dict.Clear();

1.3 LINQ

1.3.1 LINQ究竟是什么
  • LINQ和集合对象本身的方法区别:都可以操作集合对象,LINQ主要用于查询统计,而且非常非常非常强大,大多数情况下,集合的增删改使用集合本身的方法,而查询则推荐使用LINQ。而且除了集合类型外,LINQ还可以用于非常多种数据类型。
  • LINQ本质是一系列扩展方法,它有两种表达方式,查询语法和扩展语法,查询语法类似SQL,但它最后也都是转化为扩展语法,通常习惯使用扩展语法。
  • C#为多种数据类型都定义了LINQ扩展方法,比如:任何实现了 IEnumerable 接口的类型(意味着所有数组和集合)、JSON对象、XML文档,EFCore中的数据上下文,以及现在比较少用到的SQL数据库上下文、ADO中的DataSet。
  • EFCore中的LINQ:EfCore使用了LINQ的语法,定义在Microsoft.EntityFrameworkCore命名空间下,所以EFcore中的where方法和集合调用的where,虽然用法一样,但却是不同的方法,两者命名空间都不一样。使用LINQ操作数组集合时,返回IEnumerable对象,而EFCore返回IQueryable集合。两者最大区别为:数组集合会立即在服务器内存中执行计算;而EFCore会延迟执行,只有当我们执行EFCore的立即执行方法后,EFCore才会将之前定义的所有非立即执行方法,整合为SQL抛到数据库执行。
  • SugarSql中的LINQ:类似EFCore,也是以LINQ语法为基本,定义了一系列扩展方法来操作SugarSql对象。
  • 本节涉及到后续章节知识点,建议再回读。
1.3.2 LINQ语法本质
//本例中,虽然没有使用扩展方法来实现LINQ,但套入扩展方法的原理是一样的
//关于委托和扩展方法,详见《函数方法》章节
public class Program
{
    static void Main(string[] args)
    {
        int[] arrays = { 1, 2, 3 };

        //调用我们定义的MyWhere方法
        var newArrays = MyWhere(arrays, n => n >= 2);
        Console.WriteLine(String.Join(",",newArrays)); //输入2,3

        //MyWhere方法的参数,一个是操作对象,一个是方法参数(使用Func委托类型)
        static IEnumerable<int> MyWhere(IEnumerable<int> nums, Func<int, bool> filter)
        {
            foreach (int n in nums)
            {
                if (filter(n))
                {
                    yield return n;
                }
            }
        }
    }
}

1.3.3 LINQ常用方法案例
//1、如前所述,每种数据类型的LINQ方法会有差异,详见相关文档===========================
//以下案例以集合对象为例
public record Employee(int Id, string Name, int Age, bool Gender, double Salary);
IEnumerable<Employee> list = new List<Employee>()
{
    new Employee(1, "张三", 18, true, 5000.00),
    new Employee(2, "李四", 20, false, 5000.00),
    new Employee(3, "王五", 45, true, 25000.00),
    new Employee(4, "赵六", 35, false, 16000.00),
    new Employee(5, "Jack", 26, true, 8000.00),
    new Employee(6, "Jim", 40, false, 15000.00),
    new Employee(7, "Tom", 28, false, 7000.00),
    new Employee(8, "Wow", 37, false, 13500.00),
    new Employee(9, "Mic", 25, true, 8500.00),
    new Employee(10, "Bill", 33, true, 13000.00)
};


//2、Where方法,数据过滤============================================================
var where1 = list.Where(e => e.Salary > 10000 && e.Age < 40);


//3、Count方法,数据条数============================================================
var count1 = list.Count(e => e.Salary > 10000 && e.Age < 40);
var count2 = list.Where(e => e.Salary > 10000 && e.Age < 40).Count();


//4、Any方法,判断是否至少有一条满足条件的数据========================================
var any1 = list.Any(e => e.Salary > 8000);
var any2 = list.Where(e => e.Salary > 8000).Any();


//5、Single、SingleOrDefault方法,获取一条数据=======================================
var single1 = list.Single(e => e.Id == 1); //结果返回一条数据
//var single2 = list.Single(e => e.Id > 1); //结果报错
//var single3 = list.Single(e => e.Id == 11); //结果报错
var single4 = list.SingleOrDefault(e => e.Id == 1); //结果返回一条数据
//var single6 = list.SingleOrDefault(e => e.Id > 1); //结果报错
var single5 = list.SingleOrDefault(e => e.Id == 11); //结果null


//6、First、FirstOrDefault方法,获取第一条数据======================================
var first1 = list.First(e => e.Id > 1); //结果返回第一条
//var first2 = list.First(e => e.Id > 11); //结果报错
var first3 = list.FirstOrDefault(e => e.Id > 1); //结果返回第一条
var first4 = list.FirstOrDefault(e => e.Id > 10);  //结果null


//7、OrderBy、OrderByDescending、ThenBy方法,排序===================================
var order1 = list.OrderBy(e => e.Age); //正向排序
var order2 = list.OrderByDescending(e => e.Age); //逆向排序
var order3 = list.OrderBy(e => e.Age).ThenBy(e => e.Salary); //根据两个字段正向排序


//8、Skip、Take方法,跳过N条获取N条==================================================
var st1 = list.Skip(5).Take(5);
var st2 = list.Skip(5).Take(10); //不够10条,从第6条开始获取到最后
var st3 = list.Skip(5); //跳过5条,一直获取到最后
var st4 = list.Take(5); //从第1条开始,获取5条


//9、Max、Min、Average、Sum方法,聚和函数============================================
var max1 = list.Max(e => e.Age);
var max2 = list.Where(e => e.Gender == true).Max(e => e.Age);
var avg1 = list.Average(e => e.Salary);
var avg2 = list.Where(e => e.Id >2).Average(e => e.Salary);
int[] scores = { 65, 80, 90 };
var sum1 = scores.Sum();


//10、GroupBy方法,分组=============================================================
var group1 = list.GroupBy(e => e.Gender);
foreach (var item in group1)
{
    bool gender = item.Key;
    int count = item.Count();
    int maxAge = item.Max(e => e.Age);
    double avgSalary = item.Average(e => e.Salary);
    Console.WriteLine($"{gender}{count}{maxAge}{avgSalary}。");
}


//11、Select方法,投影(选取字段)===================================================
var ages = list.Select(e => e.Age);
var genders = list.Select(e => e.Gender ? "男" : "女");
var newList1 = list.Select(e => new { e.Name, e.Age, 
                                      XingBie = e.Gender ? "男" : "女" }); //匿名类型


//12、ToArray、ToList方法,集合转换,IEnumerable<T>转换为数组或List集合===============
Employee[] array1 = list.Where(e => e.Salary > 8000).ToArray();
List<Employee> list1 = list.Where(e => e.Salary > 8000).ToList();


//13、链式调用======================================================================
//Where、Select、OrderBy、GroupBy、Take、Skip等返回IEnumerable<T>的方法,可以链式调用
var linkList = list.Where(e => e.Id >= 2)
                   .GroupBy(e => e.Gender)
                   .OrderBy(g => g.Key)
                   .Take(3)
                   .Select(g => new {XingBie = g.Key?"男":"女", 
                                     AvgSalary = g.Average(e => e.Salary)});
foreach (var item in linkList)
{
    Console.WriteLine(item);
}


//14、多个集合操作==================================================================
//操作集合时较少需要使用多集合操作,多用在EFCore操作数据表时
//由于我习惯用SugarSql,所以多个集合操作的方法,我也不熟,了解一下

//14.1 Join,根据两个集合的共同键将它们的元素关联起来------------------
var students = new List<Student>
{
    new Student { Id = 1, Name = "Alice" },
    new Student { Id = 2, Name = "Bob" },
    new Student { Id = 3, Name = "Charlie" }
};

var grades = new List<Grade>
{
    new Grade { StudentId = 1, Grade = "A" },
    new Grade { StudentId = 2, Grade = "B" },
    new Grade { StudentId = 3, Grade = "A" }
};

var studentGrades = students.Join(
    grades, 
    student => student.Id,
    grade => grade.StudentId,
    (student, grade) => new { student.Name, grade.Grade }
);

//14.2 GroupJoin,分组关联--------------------------------------------------
var categories = new List<string> { "Fruit", "Vegetable" };
var products = new List<string> { "Apple", "Banana", "Carrot" };

var groupedJoin = categories.GroupJoin(
    products,
    category => category,
    product => product == "Apple" || product == "Banana" ? "Fruit" : "Vegetable",
    (category, prodGroup) => new { Category = category, Products = prodGroup });

//14.3 Union,并集--------------------------------------------------
List<int> first = new List<int> { 1, 2, 3 };
List<int> second = new List<int> { 3, 4, 5 };
var union = first.Union(second); //1,2,3,4,5

//14.4 Intersect,交集----------------------------------------------
List<int> first = new List<int> { 1, 2, 3 };
List<int> second = new List<int> { 3, 4, 5 };
var intersect = first.Intersect(second); //3

//14.5 Except,差集--------------------------------------------------
List<int> first = new List<int> { 1, 2, 3 };
List<int> second = new List<int> { 3, 4, 5 };
var except = first.Except(second); //1,2

//14.6 Concat,连接两个集合------------------------------------------
List<int> first = new List<int> { 1, 2, 3 };
List<int> second = new List<int> { 3, 4, 5 };
var concatenated = first.Concat(second); //1,2,3,3,4,5

//14.7 Zip,配对---------------------------------------------------
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
List<int> scores = new List<int> { 90, 85, 88 };
var zipped = names.Zip(scores, (name, score) => new { Name = name, Score = score });
//Name: Alice, Score: 90......

二、TS/UTS的数组和集合

2.1 数组

2.1.1 数组的定义

JS的数组是完全动态的,元素数量可以任意增减、元素类型也不限。TS增加了类型,但元素数量仍然是动态的,类似于C#中的List。同时,TS可以通过联合类型或者any类型,使元素类型多样化。UTS限制了联合类型,所以它的数组会更具有强类型特征。

//1、基本定义========================================================================
//方括号方式-----------------------------------
let arr1:number[] = [1, 2, 3]; //单一类型数组
let arr2:(number|string)[] = [1, 2, "MC"]; //联合类型数组
let arr3:any[]; //任意类型,和JS中的数组没有区别
//泛型方式---------------------------------------
let arr4:Array<number> =  [1, 2, 3];
let arr2:Array<number|string> = [1, 2, "MC"];

//数组元素不限-----------------------------------
let arr:number[];
arr = [];
arr = [1];
arr = [1, 2];
arr = [1, 2, 3];
arr[3] = 4; //arr变为 [1, 2, 3, 4]
arr.length = 2;//改变数组的长度,arr变为 [1, 2]


//2、数组的类型推断==================================================================
const arr = []; //推断为any[]
//赋值后会自动更新类型推断
arr.push(123); //推断更新为number[]
arr.push('abc'); //推断更新 (string | number)[]

//以上类型推断更新,只发生在初始化为空数组情况
const arr = [123]; //初始化后,推断类型为number[]
arr.push('abc'); //由于初始化时,确定了类型,所以报错


//3、只读数组,没用过,了解一下=======================================================
const arr:readonly number[] = [0, 1];
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];

//4、多维数组
var multi:number[][] = [[1,2,3], [23,24,25]];

2.1.1 数组的常用API
const numbers: number[] = [1, 2, 3, 4, 5];

//1、常用API=========================================================================
//1.1 添加和移除元素。原数组改变----------------------------------------
numbers.push(6); // [1, 2, 3, 4, 5, 6]
numbers.pop(); // [1, 2, 3, 4, 5]
numbers.shift(); // [2, 3, 4, 5]
numbers.unshift(0); // [0, 2, 3, 4, 5]

//1.2 合并数组。返回新数组,原数组不变-----------------------------------
const moreNumbers = [6, 7, 8];
const combined = numbers.concat(moreNumbers); // [0, 2, 3, 4, 5, 6, 7, 8]

//1.3 连接成字符串------------------------------------------------------
const str = numbers.join('-'); // "0-2-3-4-5"

//1.4 截取数组。返回新数组,原数组不变------------------------------------
const sliced = numbers.slice(1, 3); // [2, 3]

//1.5 修改数组。原数组改变------------------------------------------------
const spliced = numbers.splice(1, 2, 9, 10); // [2, 3], numbers = [0, 9, 10, 4, 5]

//1.6 查找元素,返回元素索引-----------------------------------------------
const index = numbers.indexOf(4); // 3
const lastIndex = numbers.lastIndexOf(4); // 3

//1.7 遍历数组。遍历数组元素,使用元素执行一些逻辑---------------------------
numbers.forEach((num) => console.log(num)); // 0, 9, 10, 4, 5

//1.8 映射数组。遍历数组元素,根据元素云计算并返回一个新数组------------------
const doubled = numbers.map((num) => num * 2); // [0, 18, 20, 8, 10]

//1.9 过滤数组。过滤数组元素,返回一个新数组---------------------------------
const filtered = numbers.filter((num) => num > 5); // [9, 10]

//1.10 遍历计算数组,实现累进统计-------------------------------------------
//首次递归时,参数acc为初始值0,参数num为第一个元素
//第二次,参数acc为首次递归时Lambda方法体的计算结果,即0+第一个元素;参数num为第二个元素
//......
const sum = numbers.reduce((acc, num) => acc + num, 0); // 28

//1.11 判断数组元素----------------------------------------------------------
const hasNegative = numbers.some((num) => num < 0); // false
const allPositive = numbers.every((num) => num > 0); // false

//1.12 查找元素-------------------------------------------------------------
const found = numbers.find((num) => num > 5); // 9
const foundIndex = numbers.findIndex((num) => num > 5); // 1

//1.13 包含元素-------------------------------------------------------------
const contains = numbers.includes(4); // true

//1.14 排序和反转-----------------------------------------------------------
const sorted = numbers.sort((a, b) => a - b); // [0, 4, 5, 9, 10]
const reversed = numbers.reverse(); // [10, 9, 5, 4, 0]



//2、没用过的API====================================================================
//2.1 填充数组,参数依次为“填充值,开始索引,结束索引”-------------------------
numbers.fill(1, 1, 3); // [10, 1, 1, 4, 0]

//2.2 复制数组内部元素,参数依次为--------------------------------------------
numbers.copyWithin(0, 3, 5); // [4, 0, 1, 4, 0]

//2.3 展开多维数组-----------------------------------------------------------
const nestedNumbers = [1, [2, [3, [4]], 5]];
const flatNumbers = nestedNumbers.flat(2); // [1, 2, 3, [4], 5]

//2.4 映射并压缩-------------------------------------------------------------
const flatMapped = numbers.flatMap((num) => [num, num * 2]); 
// [4, 8, 0, 0, 1, 2, 4, 8, 0, 0]



//3、另外两种遍历方式,之前讲过,重复一下=============================================
//3.1 for...of,遍历元素
let numbers: number[] = [10, 20, 30, 40, 50];
for (let num of nmbers) {
  console.log(num);
}

//3.2 for...in,遍历键
const person: {name:string;age:number} = {
  name: "John",
  age: 30
};
for (const key in person) {
  console.log(`Key: ${key}, Value: ${person[key]}`);
}

2.2 Set

Set可以认为是能够自动去除重复值的数组,只能通过new Set()构造函数创建。

//1、定义和初始化Set=================================================================
let mySet1: Set<number> = new Set(); //初始化,没有元素
let mySet: Set<number> = new Set([1, 2, 3]); //初始化时添加元素
let mySet2 = new Set("abcdd"); //参数除了数组,还可以是任何具有iterable接口的数据结构



//2、Set的基本操作===================================================================
//2.1 添加元素-----------------------
// 注意:向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值
mySet.add(4); //添加单个元素
mySet.add(5).add(6); //链式增加

//2.2 删除元素------------------------
mySet.delete(3);
console.log(mySet.has(3)); //false

//2.3 读取元素------------------------
//不能直接读取,麻烦点
//方法一:转换为数组后读取
let myArray = Array.from(mySet);
let firstValue = myArray[0];
//方法二:使用展开运算符转化为数组
let firstValue = [...mySet][0]
//方法三:使用迭代器(关于迭代器,在后续章节说明)
let iterator = mySet.values(); //返回值的迭代器
let firstValue = iterator.next().value; //读取第一个值
let secondValue = iterator.next().value; //读取第二个值

//2.4 获取大小------------------------
console.log(mySet.size); //6

//2.5 检查元素-------------------------
console.log(mySet.has(4)); //true
console.log(mySet.has(7)); //false

//2.6 清空Set-------------------------
mySet.clear();
console.log(mySet.size); //0



//3、遍历Set========================================================================
let set = new Set(['red', 'green', 'blue']);
//3.1 使用forEach实例方法------------------------
mySet.forEach((value) => {
  console.log(value);
});

//3.2 使用Set的迭代器
//有三个,分别为keys、values、entries,但Set没有键,所以键和值是一样的
for (let item of set.keys()) {
  console.log(item); //red green blue
}
for (let item of set.values()) {
  console.log(item); //red green blue
}
for (let item of set.entries()) {
  console.log(item); //["red", "red"] ["green", "green"] ["blue", "blue"]
}
//不用keys、values、entries,和数组一样直接遍历
for (let x of set) {
  console.log(x); //red green blue
}



//4、其它骚操作======================================================================
//4.1 去除重复数据-------------------------------
[...new Set(array)] //去除数组的重复成员
[...new Set('ababbc')].join('') //去除重复字符

//4.2 实现并集、交集、差集------------------------
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
let union = new Set([...a, ...b]); // 并集 {1, 2, 3, 4}
let intersect = new Set([...a].filter(x => b.has(x))); // 交集 {2, 3}
let difference = new Set([...a].filter(x => !b.has(x))); //差集 {1}

//4.3 遍历并改变原Set-----------------------------
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2)); //set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2)); //set的值是2, 4, 6

2.3 Map

Map是一种键值对集合,类似于普通的对象,但Map允许键的类型不限于字符串。

//1、定义和初始化Map=================================================================
let myMap: Map<number, string> = new Map(); //定义一个Map并初始化
//在初始化时添加键值对,类似二元数组
let myMapWithElements: Map<number, string> = new Map([ 
  [1, 'one'],
  [2, 'two'],
  [3, 'three']
]);
//键可以是任何类型,甚至可以是对象类型(C#中只有可序列化类型可以作为键)
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"



//2、基本操作========================================================================
//2.1 添加或更新键值对------------------------
myMap.set(4, 'four');
myMap.set(5, 'five').set(6, 'six');

//2.2 获取值----------------------------------
console.log(myMap.get(4)); // 输出: four
console.log(myMap.get(7)); // 输出: undefined

//2.3 检查是否包含某个键-----------------------
console.log(myMap.has(4)); // 输出: true
console.log(myMap.has(7)); // 输出: false

//2.4 获取大小---------------------------------
console.log(myMap.size); // 输出: 6

//2.5 删除键值对-------------------------------
myMap.delete(3);
console.log(myMap.has(3)); // 输出: false

//2.6 清空Map----------------------------------
myMap.clear();
console.log(myMap.size); // 输出: 0



//3、遍历Map========================================================================
const myMap = new Map([ ['F', 'no'], ['T',  'yes']]);
//3.1 使用forEach方法----------------
myMap.forEach((value, key) => {
  console.log(`${key}: ${value}`); // 依次输出"F": "no"  "T": "yes"
});

//3.2 使用迭代器--------------------
for (let key of map.keys()) {
  console.log(key); // "F" "T"
}
for (let value of map.values()) {
  console.log(value); // "no" "yes"
}
for (let item of map.entries()) {
  console.log(item[0], item[1]); // "F" "no" "T" "yes"
}
for (let [key, value] of map.entries()) {
  console.log(key, value); 
}
for (let [key, value] of map) {
  console.log(key, value); //等同于使用entries(), "F" "no" "T" "yes"
}



//4、Map数据类型转换=================================================================
//4.1 Map和二维数组互转---------------------
//使用new Map构造函数,将二维数组转为Map
const myMap = new Map([
  [true, 7],
  {foo: 3}, ['abc']
]);
//使用扩展运算符,将Map转为二维数组
[...myMap] //[ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]


//4.2 Map和对象互转--------------------------
//对象转为Map
let obj = {"a":1, "b":2};
let myMap = new Map(Object.entries(obj));
//Map转为对象
let obj = Object.create(null);
for (let [k,v] of myMap) {
  obj[k] = v;
}


//4.3 键名是字符串,Map和JSON互转---------------------------
//4.3.1 定义一个Map类型数据
let myMap = new Map<string, number>();
myMap.set("a", 1);
myMap.set("b", 2);
myMap.set("c", 3);

//4.3.2 将Map转换为JSON
let mapAsArray = Array.from(myMap.entries()); //先将Map转换为二维数组
let jsonString = JSON.stringify(mapAsArray); //再将二维数组转为JSON

//4.3.3 将JOSN转换为Map
let parsedArray: [string, number][] = JSON.parse(jsonString); //先将JOSN转为二组数组
let newMap = new Map<string, number>(parsedArray); //再将二维数组转换为Map对象


//4.4 键名是非字符串,Map和JSON互转--------------------------
//4.4.1 定义一个Map类型数据
let myMap = new Map<number, string>();
myMap.set(1, "one");
myMap.set(2, "two");
myMap.set(3, "three");

//4.4.2 将Map转换为JSON
let mapAsArray = Array.from(myMap.entries()) //先将Map转换为二维数组
                      .map(([key, value]) => [key.toString(), value]); //修改键的类型
let jsonString = JSON.stringify(mapAsArray); //再将二维数组转为JSON

//4.4.3 将JOSN转换为Map
let jsonString = '[["1","one"],["2","two"],["3","three"]]';
let parsedArray: [string, string][] = JSON.parse(jsonString); // 将JSON解析为二维数组
let myMap = new Map<number, string>( //将二维数组转换为Map对象
  parsedArray.map(([key, value]) => [Number(key), value]) //修改键类型
);

三、补充说一下元组(C#/TS)

C#和TS中都有元组,将多个不同类型的元素组合为一个单一的数据对象。在方法需要返回多个值,或者局部范围内传递多个值时,如果不想专门定义一个类型,可以考虑使用,比如前端的事件参数。我在实际开发中,基本不用,原因有二:一是使用类或者对象对不同类型数据进行封装,更加见名知意,定义类型的工作值得付出;二是元组代码很丑陋(很难读),尤其是作为方法的参数和返回值类型使用时。

3.1 C#中使用元组

C#中的元组具有不可变性,不能修改。

class Program
{
    static void Main()
    {
        //创建元组方式一:使用静态方法----------------------------
        var tuple1 = Tuple.Create(1, "Hello");
        Console.WriteLine(tuple1.Item1); // 1
        Console.WriteLine(tuple1.Item2); // Hello

        //创建元组方式二:使用元组表达式。***推荐-----------------
        var tuple2 = (1, "Hello");
        Console.WriteLine(tuple2.Item1); // 1
        Console.WriteLine(tuple2.Item2); // Hello

        //创建元组时,可以命名元素--------------------------------
        var tuple3 = (Id: 1, Name: "Hello");
        Console.WriteLine(tuple3.Id);   // 1
        Console.WriteLine(tuple3.Name); // Hello

        //解构元组----------------------------------------------
        var (id, name) = tuple3;
        Console.WriteLine(id);   // 1
        Console.WriteLine(name); // Hello

        //调用返回元组的方法------------------------------------
        var person = GetPerson();
        Console.WriteLine(person.Item1); // 1
        Console.WriteLine(person.Item2); // John Doe
      
        //调用返回命名元素元组的方法-----------------------------
        var namedPerson = GetPersonWithNames();
        Console.WriteLine(namedPerson.Id);   // 1
        Console.WriteLine(namedPerson.Name); // John Doe
    }
  
    //元组作为方法返回值的类型。你瞅瞅,多丑哇,反正我是不用的。
    public static (int, string) GetPerson()
    {
        return (1, "John Doe");
    }
  
    //命名元素的元组作为方法返回值的类型
    public static (int Id, string Name) GetPersonWithNames()
    {
        return (1, "John Doe");
    }
}

3.2 TS中使用元组

TS中的元组可以修改,还不清楚UTS中是否支持元组。

//TS中的元组,因为使用类型注解的形式,相比较C#,看起来会舒服点

//创建元组-----------------------------------------------
let tuple: [number, string, boolean];
tuple = [1, "hello", true]; // 正确
// tuple = ["hello", 1, true]; // 错误,类型不匹配


//访问和修改元组------------------------------------------
console.log(tuple[0]); // 输出: 1
console.log(tuple[1]); // 输出: "hello"
console.log(tuple[2]); // 输出: true
tuple[1] = "world";
console.log(tuple[1]); // 输出: "world"


//解构元组------------------------------------------------
let tuple: [number, string, boolean] = [1, "hello", true];
let [num, str, bool] = tuple;
console.log(num);  // 输出: 1
console.log(str);  // 输出: "hello"
console.log(bool); // 输出: true


//函数返回多个值-------------------------------------------
function getPerson(): [number, string] {
    return [1, "John Doe"];
}
let [id, name] = getPerson();
console.log(id);   // 输出: 1
console.log(name); // 输出: "John Doe"


//用元组表示键值对----------------------------------------
let keyValue: [string, number] = ["age", 30];
console.log(keyValue[0]); // 输出: "age"
console.log(keyValue[1]); // 输出: 30


//结合接口或type使用--------------------------------------
type Person = [number, string];
let person: Person = [1, "John Doe"];

interface Point {
    coordinates: [number, number];
}
let point: Point = {
    coordinates: [10, 20]
};

console.log(point.coordinates); // 输出: [10, 20]

  • 29
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值