C#.NET 范围与索引(Range、Index)完全解析:语法、用法与最佳实践

作者:唐青枫日期:2025/11/27

简介

C# 8.0 引入了范围(Ranges)和索引(Indices)功能,提供了更简洁、更直观的语法来处理集合中的元素和子集。这些功能大大简化了数组、字符串、列表等数据结构的操作。

索引(Indices)

从末尾开始的索引

使用 ^ 运算符表示从末尾开始的索引:

1int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
2
3// 传统方式获取最后一个元素
4int last1 = numbers[numbers.Length - 1]; // 9
5
6// 使用索引运算符获取最后一个元素
7int last2 = numbers[^1]; // 9
8
9// 获取倒数第二个元素
10int secondLast = numbers[^2]; // 8
11
12// 获取倒数第三个元素
13int thirdLast = numbers[^3]; // 7
14

索引的工作原理

索引实际上是 System.Index 结构体的语法糖:

1// 以下两行代码是等价的
2int last = numbers[^1];
3int last = numbers[new Index(1, fromEnd: true)];
4
5// 从开头开始的索引
6int first = numbers[0]; // 等价于 numbers[new Index(0, fromEnd: false)]
7
表达式含义等同于
^0序列结束后的位置array.Length
^1最后一个元素array[array.Length - 1]
^2倒数第二个元素array[array.Length - 2]
^n从末尾算起的第 n 个元素array[array.Length - n]

范围(Ranges)

基本范围操作

使用 .. 运算符指定范围:

1int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
2
3// 获取索引1到3的元素(不包括索引3)
4int[] sub1 = numbers[1..3]; // [1, 2]
5
6// 获取从开始到索引3的元素
7int[] sub2 = numbers[..3]; // [0, 1, 2]
8
9// 获取从索引6到末尾的元素
10int[] sub3 = numbers[6..]; // [6, 7, 8, 9]
11
12// 获取所有元素
13int[] all = numbers[..]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
14
15// 使用从末尾开始的索引定义范围
16int[] sub4 = numbers[^3..^1]; // [7, 8] - 从倒数第三个到倒数第一个(不包括)
17

范围的工作原理

范围实际上是 System.Range 结构体的语法糖:

1// 以下两行代码是等价的
2int[] sub = numbers[1..4];
3int[] sub = numbers[new Range(1, 4)];
4
5// Range 包含 Start  End 两个 Index
6Range range = 1..4;
7Console.WriteLine($"Start: {range.Start}, End: {range.End}");
8// 输出: Start: 1, End: 4
9
语法含义等同于
..整个范围[0..^0]
start..从 start 到序列结束[start..^0]
..end从开始到 end 之前[0..end]
start..end从 start 到 end 之前[start..end]
^start..^end使用末尾索引指定范围[length - start..length - end]

范围表达式返回值

范围表达式返回的是原序列的视图(view),而不是副本。对于数组、字符串等类型,它返回的是只读视图;对于 Span<T>Memory<T>,它返回新的 SpanMemory

1int[] original = [1, 2, 3, 4, 5];
2int[] slice = original[1..4]; // [2, 3, 4]
3
4// 修改原始数组会影响切片
5original[2] = 100;
6Console.WriteLine(string.Join(", ", slice)); // 2, 100, 4
7
8// 修改切片也会影响原始数组
9slice[1] = 200;
10Console.WriteLine(string.Join(", ", original)); // 1, 2, 200, 4, 5
11

不同类型的使用示例

数组

1int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
2
3// 获取前5个元素
4int[] firstFive = array[..5]; // [0, 1, 2, 3, 4]
5
6// 获取最后3个元素
7int[] lastThree = array[^3..]; // [7, 8, 9]
8
9// 获取中间部分
10int[] middle = array[3..7]; // [3, 4, 5, 6]
11
12// 获取除第一个和最后一个之外的所有元素
13int[] withoutEnds = array[1..^1]; // [1, 2, 3, 4, 5, 6, 7, 8]
14

字符串

1string text = "Hello, World!";
2
3// 获取前5个字符
4string hello = text[..5]; // "Hello"
5
6// 获取最后6个字符
7string world = text[^6..]; // "World!"
8
9// 获取逗号后的部分(不包括逗号本身和空格)
10string afterComma = text[7..^1]; // "World"
11
12// 获取子字符串
13string sub = text[7..12]; // "World"
14

列表(List<T>

1List<int> list = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
2
3// 获取范围(需要转换为数组或使用GetRange)
4int[] subArray = list.ToArray()[2..6]; // [2, 3, 4, 5]
5
6// 或者使用List的GetRange方法(不是基于范围的语法,但功能类似)
7List<int> subList = list.GetRange(2, 4); // [2, 3, 4, 5]
8

Span<T>Memory<T>

1int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
2
3// 创建Span并使用范围
4Span<int> span = array.AsSpan();
5Span<int> spanSlice = span[2..6]; // [2, 3, 4, 5]
6
7// 创建Memory并使用范围
8Memory<int> memory = array.AsMemory();
9Memory<int> memorySlice = memory[3..7]; // [3, 4, 5, 6]
10

高级用法和模式

与模式匹配结合使用

1int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
2
3// 使用范围进行模式匹配
4if (numbers is [0, 1, 2, .., 8, 9])
5{
6    Console.WriteLine("数组以0,1,2开头,以8,9结尾");
7}
8
9// 在switch表达式中使用
10string result = numbers switch
11{
12    [0, 1, 2, ..] => "以0,1,2开头",
13    [.., 7, 8, 9] => "以7,8,9结尾",
14    [0, .., 9] => "以0开头,以9结尾",
15    _ => "其他模式"
16};
17

自定义类型支持范围

要使自定义类型支持范围操作,需要实现以下方法之一:

1public class MyCollection<T>
2{
3    private T[] _items;
4    
5    public MyCollection(T[] items)
6    {
7        _items = items;
8    }
9    
10    // 方法1:实现Slice方法
11    public MyCollection<T> Slice(int start, int length)
12    {
13        T[] slice = new T[length];
14        Array.Copy(_items, start, slice, 0, length);
15        return new MyCollection<T>(slice);
16    }
17    
18    // 方法2:实现索引器接受Range参数
19    public MyCollection<T> this[Range range]
20    {
21        get
22        {
23            var (start, length) = GetStartAndLength(range);
24            return Slice(start, length);
25        }
26    }
27    
28    // 辅助方法:将Range转换为(start, length)
29    private (int start, int length) GetStartAndLength(Range range)
30    {
31        int start = range.Start.IsFromEnd ? 
32            _items.Length - range.Start.Value : range.Start.Value;
33        
34        int end = range.End.IsFromEnd ? 
35            _items.Length - range.End.Value : range.End.Value;
36        
37        int length = end - start;
38        return (start, length);
39    }
40    
41    // 其他成员...
42}
43
44// 使用自定义集合的范围操作
45var collection = new MyCollection<int>(new[] { 0, 1, 2, 3, 4, 5 });
46var subCollection = collection[1..4]; // 包含元素[1, 2, 3]
47

实际应用场景

字符串处理

1// 提取文件扩展名
2string GetFileExtension(string filename)
3{
4    int dotIndex = filename.LastIndexOf('.');
5    return dotIndex >= 0 ? filename[(dotIndex + 1)..] : string.Empty;
6}
7
8// 提取域名
9string GetDomain(string url)
10{
11    int protocolEnd = url.IndexOf("://");
12    if (protocolEnd < 0) return url;
13    
14    int domainStart = protocolEnd + 3;
15    int pathStart = url.IndexOf('/', domainStart);
16    
17    return pathStart < 0 ? 
18        url[domainStart..] : 
19        url[domainStart..pathStart];
20}
21
22// 处理CSV行
23string[] ParseCsvLine(string line)
24{
25    List<string> fields = new List<string>();
26    int start = 0;
27    
28    while (start < line.Length)
29    {
30        int end = line.IndexOf(',', start);
31        if (end < 0) end = line.Length;
32        
33        fields.Add(line[start..end].Trim());
34        start = end + 1;
35    }
36    
37    return fields.ToArray();
38}
39

数据分页

1// 使用范围实现分页
2public IEnumerable<T> GetPage<T>(T[] data, int pageNumber, int pageSize)
3{
4    int startIndex = (pageNumber - 1) * pageSize;
5    if (startIndex >= data.Length)
6        return Enumerable.Empty<T>();
7    
8    int endIndex = Math.Min(startIndex + pageSize, data.Length);
9    return data[startIndex..endIndex];
10}
11
12// 使用Span<T>提高性能
13public ReadOnlySpan<T> GetPageSpan<T>(T[] data, int pageNumber, int pageSize)
14{
15    int startIndex = (pageNumber - 1) * pageSize;
16    if (startIndex >= data.Length)
17        return ReadOnlySpan<T>.Empty;
18    
19    int endIndex = Math.Min(startIndex + pageSize, data.Length);
20    return data.AsSpan()[startIndex..endIndex];
21}
22

数组操作

1// 数组旋转
2void RotateArrayLeft<T>(T[] array, int positions)
3{
4    positions %= array.Length;
5    if (positions == 0) return;
6    
7    // 创建临时数组保存前positions个元素
8    T[] temp = array[..positions];
9    
10    // 将剩余元素向左移动
11    Array.Copy(array, positions, array, 0, array.Length - positions);
12    
13    // 将临时数组中的元素放回末尾
14    Array.Copy(temp, 0, array, array.Length - positions, positions);
15}
16
17// 数组分割
18(T[] left, T[] right) SplitArray<T>(T[] array, int splitIndex)
19{
20    return (array[..splitIndex], array[splitIndex..]);
21}
22

C#.NET 范围与索引(Range、Index)完全解析:语法、用法与最佳实践》 是转载文章,点击查看原文


相关推荐


VI视频输入模块学习
浅笑离愁12342025/11/24

文章目录 1.RV1126的视频输入vi模块1.1什么是VI模块1.2RV1126的VI模块重要参数 2.RV1126的VI模块初始化API2.1RK_MPI_VI_SetChnAttr讲解: 1.RV1126的视频输入vi模块 V4L2是Video for linux2的简称,为linux中关于视频设备的内核驱动。在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写 1.1什么是VI模块 RV1126的VI模块指的是视频输入模块,它的


Redis_16_哨兵
寻梦缘62025/11/23

在我们上篇文章讲的主从复制模式下,主节点一旦挂了,就需要人工进行主从切换,并且需要将新的主节点通知给客户端。这会大大影响系统的可用性,毕竟靠人工修复这个事情是很不靠谱的,先不说要耗费大量的时间;还有可能配置错误,导致出现问题。 哨兵机制就是通过自动化的手段来解决主节点挂了的问题~~ 基本概念 哨兵进程是通过独立的进程来体现的,和之前redis-server是不同的进程!!redis-sentinel不负责存储数据,只是对redis-server起到监控的效果。而且这里的哨兵节点,也会搞一个


「干货长文」强化学习完全指南:从基础MDP到TRPO/PPO/GRPO算法演进
智见AGI2025/11/21

随着 gpt-o1出现以及 DeepSeek-R1 的技术开源,强化学习从以谷歌 DeepMind 团队为主的游戏领域,以及与传统控制相结合的具身智能机器人领域,走上了LLM甚至多模态的行业赛道。通过复杂任务拆解和奖励为导向的迭代训练大幅提升了大模型解决复杂问题能力,泛化性以及动态调整能力。Reinforce Learning 带领 LLM 步入 2.0 时代,继 PPO 之后,最近关于梯度优化(Policy Optimization)算法的创新也是层出不穷,GRPO,DAPO,CISPO 等


TypeScript 简史:它是怎么拯救我的烂代码的
也无风雨也雾晴2025/11/19

看烂代码的场景 接手老旧 JavaScript 项目的时候,盯着屏幕上的一行代码发呆,这种绝望你一定体会过: JavaScript function process(data) { return data.value + 10; // 此时 data 是 undefined,程序崩了 } 看着这个 data,我满脑子都是问号: 它是对象还是数字? 到底是谁传进来的? 我要是改了它,会不会导致隔壁模块的页面挂掉? 为什么明明是字符串 '10',结果拼成了 '1010'? 这时候我就在


从Docker到宝塔:Magento2 2.3.5 安装全流程踩坑与成功实践
Linux运维技术栈2025/11/18

前言:Magento2 作为 Adobe 旗下的企业级开源电商平台,凭借强大的扩展性、多语言支持和精细化运营功能,成为全球众多电商企业的首选。但它的安装配置门槛较高,尤其对新手而言,环境兼容、依赖管理、静态资源部署等环节极易踩坑——我就曾因盲目尝试 Docker 容器部署,遭遇了版本冲突、权限隔离、内存溢出等一系列问题,耗费大量时间仍未成功。 后来转向操作更直观的宝塔面板,在反复调试中解决了 Composer 1.x 降级、内存限制解除、静态文件 404、CSS/JS 缺失等核心痛点,最终摸索出


Python 的内置函数 set
IMPYLH2025/11/17

Python 内建函数列表 > Python 的内置函数 set Python 的内置函数 set() 是一个非常有用的数据结构,用于创建无序且不包含重复元素的集合。它提供了高效的成员检测和元素去重功能,是处理集合运算的理想选择。 基本用法 创建集合: # 使用花括号 fruits = {'apple', 'banana', 'orange'} # 使用set()函数 numbers = set([1, 2, 3, 2, 1]) # 结果为{1, 2, 3} 空集合创建: emp


Python 的内置函数 ord
IMPYLH2025/11/16

Python 内建函数列表 > Python 的内置函数 ord Python 的内置函数 ord() 是一个非常有用的字符串处理函数,它主要用于获取单个字符的 Unicode 码点值。具体来说,ord() 函数接受一个长度为 1 的字符串(即单个字符)作为参数,并返回该字符对应的 Unicode 码点的整数数值。 语法格式: ord(c) 其中 c 是一个表示单个字符的字符串。 主要特点: 只能处理单个字符,如果传入字符串长度超过1会抛出 TypeError对 ASCII 字符返


Redis(125)Redis在社交网络中的应用有哪些?
Victor3562025/11/15

在社交网络中,Redis 被广泛应用于以下场景: 用户会话管理 好友关系存储 消息队列 活动流 点赞和评论统计 排行榜 下面详细介绍这些应用场景,并结合 Java 代码示例进行讲解。 1. 用户会话管理 Redis 可以用于管理用户会话,通过 Redis 的高效性和持久化机制,能够快速读取和写入会话数据。 示例代码 import redis.clients.jedis.Jedis; public class SessionManagementExample { public sta


OkHttp网络框架设计
杨充2025/11/13

OkHttp网络框架设计 目录介绍 01.整体概述介绍 1.1 概述介绍 1.2 核心特性说明 1.3 技术架构概览 1.4 问题思考 02.核心架构设计 2.1 整体架构设计 2.2 整体设计思路 2.3 核心组件关系图 03.核心组件详解 3.1 OkHttpClient 3.2 Request请求封装 3.3 Call请求执行接口 3.4 Dispatcher调度器 3.5 拦截器机制 3.6 Response返回 04.核心流程分析 4.1 请求执行流程 4.2 连


PyTorch:AI深度学习开源框架
深圳蔓延科技2025/11/12

如果把构建一个AI模型比作搭建一个复杂的乐高城堡,那么 PyTorch 就是一个为你提供了各种基础积木,并且让你能非常自由、顺手地去拼接它们的工具箱。 它不像一些已经搭好的成品玩具(比如一些封装好的软件),你只能看不能改。PyTorch 的魅力在于它的 “动态” 和 “直观”。 1. 核心概念一:张量 - 其实就是“数据容器” 你可能听过一个词叫 “张量”(Tensor)。听起来很高深,但其实它就是 PyTorch 里最基本的数据容器。 你可以这样理解: 标量(一个数):就是一个零维张量。比如

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2025 聚合阅读