2024年7月

这几年科技变革太快,AI更是飞速发展,作为一名IT老兵,使用过的搜索引擎也是一换再换。这不,刚消停了一段时间的OpenAI又丢出一个炸弹SearchGPT,直接跟谷歌掀桌子了。

1、谷歌搜索的无奈

早年只能用用百度搜索或者其余小众搜索,虽说有不少广告,搜索到的东西也不够精准,只能忍着了。

后来找了梯子,用过微软
bing
搜索、谷歌搜索,总体来说更喜欢谷歌搜索,即精准又没广告。谷歌搜索应该是程序员最喜欢的搜索引擎了。

23年年初OpenAI推出ChatGPT,顺势加入第一批试用等待队列,当时的注册过程比较麻烦,既要用梯子又要用美区的接码平台。

试用了ChatGPT后,欲罢不能,解决问题的体验更好了。从之前的输入问题→打开各种网页→寻找答案的复杂过程,变成了输入问题→输出答案。

至此,我就很少用谷歌搜索了,只有涉及到新技术时才用一用搜索。

随着GPT-4o的推出,增强了模型能力,加入了各种插件和联网功能,我感觉AI解决问题的能力已经超过谷歌搜索了。

如果说此时的搜索还有一个优点的话,那就是搜出来的结果是一个个的网页,每个内容都是有指向有出处的。

但是SearchGPT出来了,这个优点也算没了。

2、SearchGPT的出世

6月26号凌晨,OpenAI发布了SearchGPT。看看他的官网介绍:

SearchGPT的出世,预示着,OpenAI正式进军搜索领域。

SearchGPT是一种AI搜索引擎,提供快速及时的答案以及清晰答案相关的来源。谷歌搜索做到的它都能做到,还可以提供更简洁清晰的答案。

我们借助AI提效,之前搜索一个问题寻找答案可能需要5分钟,现在估计只需要5秒钟。

之前AI给答案时,总是不给出处和来源,讲实在话,对原创作者来说还是蛮伤的。毕竟写一篇原创要花不少精力,结果被有心人利用AI一顿洗稿就拿去用了,真的是一点办法没有。

最近两年的AI问答(不是今天聊的
AI搜索
哦),导致
原创作者从之前的被尊重者一下子变成了被伤害者
,这就导致以后更是没人有动力去原创了。

我也希望SerachGPT的出世,能给创作者带去一些安慰和信心,能真正的给创作者带来保护。相信以后创作者可以开心的拥抱AI了。

3、SearchGPT的其余特性

SearchGPT除了给出答案之外,还有一些其余特性

  • 不只是给出答案,还会给出AI的理解。
  • 清晰的给出答案的来源。
  • 持续追问。如果一次提问没得到你要的答案,那就继续提问,就像与人交谈一样,每个查询都会构建共享的上下文。
  • 与出版商和创作者合作。打造一个出版商和创作者和搜索用户的生态系统。

其实AI搜索和目前的AI问答,最大的不同就是,
AI搜索会重视、尊重和保护出版商和创作者
。只有源源不断的创新创作,才能带领人类往前走。
相信SearchGPT也会给创作者在AI时代带来信心和真正的保护

如果人人都利用AI玩抄袭和玩洗稿,那所有人都是失去创作动力,长期来看,对人类文明的演进是一个巨大的伤害。OpenAI在AI搜索这事儿上算是玩的非常明白了。

OpenAI的SearchGPT这一招不仅可以从谷歌搜索里抢一部分市场,同时,也能带来不少盈利,OpenAI一直很难变现,也许这是条路子。

4、谷歌你是咋地啦

说实在话,我还是希望谷歌尽快在AI赛道找到自己的一席之地,毕竟用了多年的谷歌搜索和白嫖了多年谷歌的GPU,我还是有些感情的,哈哈。

我也知道,谷歌一直急于在搜索引擎中加入人工智能功能,想留住用户,但是一直没有搞出创新型的产品。

所以,我也想问,谷歌你是咋地啦?

本篇完结!欢迎 关注、加微信 交流、二维码如下!!!

原文链接:
https://mp.weixin.qq.com/s/Got-DZZrZRUVc_J_n-_3Xw


《FFmpeg开发实战:从零基础到短视频上线》一书的“10.2  FFmpeg推流和拉流”提到直播行业存在RTSP和RTMP两种常见的流媒体协议。除此以外,还有于2017年推出的SRT协议,相比常见的RTMP协议,SRT协议具有更低的延迟,并且消除了卡帧、抖动等花屏现象。腾讯视频云已经引入了SRT协议,在实践中取得了良好的视频直播效果。

FFmpeg从4.0开始支持集成第三方的libsrt库,该库为SRT协议的支持库。OBS Studio、MediaMTX等开源软件也纷纷支持SRT,国产的两个开源流媒体服务器SRS和ZLMediaKit都支持SRT协议,可见在直播领域SRT取代RTMP已是大势所趋。在本文的直播操作之前,务必确保电脑上的FFmpeg已经集成了libsrt库,详细的集成步骤参见之前的文章《Windows环境给FFmpeg集成libsrt》。
下面就以Windows环境为例,把OBS Studio作为SRT直播的录制推流方,把MediaMTX作为承载SRT直播的流媒体服务器,把ffplay和VLC media player作为SRT直播的拉流播放器,演示看看如何在个人电脑搭建一个从录制到分发到播放的完整SRT直播系统。
首先按照《FFmpeg开发实战:从零基础到短视频上线》一书“10.2.2  FFmpeg向网络推流”的介绍,安装并启动MediaMTX,弹出的MediaMTX日志窗口显示内容如下:

2023/12/10 14:18:25 INF MediaMTX v1.3.0
2023/12/10 14:18:25 INF configuration loaded from E:\Program Files (x86)\mediamtx\mediamtx.yml
2023/12/10 14:18:25 INF [RTSP] listener opened on :8554 (TCP), :8000 (UDP/RTP), :8001 (UDP/RTCP)
2023/12/10 14:18:25 INF [RTMP] listener opened on :1935
2023/12/10 14:18:25 INF [HLS] listener opened on :8888
2023/12/10 14:18:25 INF [WebRTC] listener opened on :8889 (HTTP), :8189 (ICE/UDP)
2023/12/10 14:18:25 INF [SRT] listener opened on :8890 (UDP)

从上面日志可见,MediaMTX提供的SRT服务正在监听8890端口,且该服务采用UDP传输层协议。此时SRT服务的访问地址前缀为srt://127.0.0.1:8890?streamid=xxx,开头的“srt://”表示该地址采用srt协议,后面的“127.0.0.1:8890”为SRT服务的IP和端口号。再后面的“?streamid=xxx”表示SRT服务的流名称,其中xxx为形如“action:path”格式的字符串,冒号前面的action有两个取值,为publish表示推流方,为read表示拉流方;冒号后面的path表示播放地址的路径名称。
接着启动OBS Studio,单击主界面右下角的“设置”按钮,弹出的设置窗口在左边列表选择“直播”,窗口右边的“服务”栏选择“自定义”,“服务器”栏填“srt://127.0.0.1:8890?streamid=publish:live”,如下图所示:

设置好SRT服务器地址后,按照之前文章《使用OBS Studio开启RTMP直播推流》的说明,让OBS Studio开启视频直播推流,观察MediaMTX的服务日志如下所示:

2023/12/10 14:18:59 INF [SRT] [conn 127.0.0.1:50021] opened
2023/12/10 14:19:01 INF [SRT] [conn 127.0.0.1:50021] is publishing to path 'live', 2 tracks (H264, MPEG-4 Audio)

可见MediaMTX的SRT服务正在把推流方的音频流和视频流发布到live路径。
然后按照《FFmpeg开发实战:从零基础到短视频上线》一书“1.3  Windows系统安装FFmpeg”的介绍,在个人电脑上安装FFmpeg并打开MSYS的命令行,运行下面的ffplay命令,期望从SRT地址拉流播放。

ffplay -i 'srt://127.0.0.1:8890?streamid=read:live'

此时ffplay弹出播放器窗口,正常播放视频画面和声音。同时观察MediaMTX的服务日志如下所示:

2023/12/10 14:19:40 INF [SRT] [conn 127.0.0.1:53466] opened
2023/12/10 14:19:40 INF [SRT] [conn 127.0.0.1:53466] is reading from path 'live', 2 tracks (H264, MPEG-4 Audio)

可见作为拉流方的ffplay正在从SRT服务的live路径拉取音频流和视频流,说明MediaMTX通过SRT协议成功实现了视频直播的SRT推拉流功能。
最后打开VLC media player,依次选择菜单:“媒体”——“打开网络串流”,在URL地址栏填入SRT服务的拉流链接“srt://127.0.0.1:8890?streamid=read:live”,单击窗口右下角的播放按钮,即可使用VLC media player播放SRT直播地址了。

更多详细的FFmpeg开发知识参见
《FFmpeg开发实战:从零基础到短视频上线》
一书。


业务场景描述

假设用户正在填写一个复杂的表单,由于表单内容繁多,用户花费了很长时间才填完。这时,如果Token已经过期,系统会让用户重新登录,这种体验显然是非常糟糕的。为了避免这种情况,我们需要在Token即将过期或已经过期时,自动刷新Token,而不影响用户正在进行的操作。

技术实现思路

一、准备工作

  1. 前端框架选择
    :本文以Vue.js为例,结合Vue Router和Axios来实现Token无感刷新机制。
  2. 后端支持
    :后端需要提供刷新Token的接口,当接收到旧的Token时返回一个新的Token。

二、关键技术点

  1. 拦截请求
    :使用Axios拦截器来检测每一个请求的状态,如果发现Token过期,则触发刷新Token的逻辑。
  2. 刷新Token
    :实现一个专门用于刷新Token的方法,该方法会在旧的Token过期时自动调用并更新Token。
  3. 请求队列
    :在刷新Token期间,暂停其他需要Token的请求,待Token刷新成功后,重新发送这些请求。

三、具体实现步骤

1. 配置Axios拦截器

首先,配置Axios拦截器来检测请求和响应的状态,并在Token过期时触发刷新Token的逻辑。

import axios from 'axios';import store from './store';// 假设使用Vuex来管理全局状态
import router from './router';let isRefreshing = false;let requests = [];axios.interceptors.request.use(
config => {
const token = store.state.token
;if (token) {
config.headers[
'Authorization'] = 'Bearer' + token;}
return config
;},
error => {
return Promise.reject(error)
;}
)
;axios.interceptors.response.use(
response => {
return response
;},
error => {
const { config, response } = error
; const originalRequest = config;if (response && response.status ===401) {
if (!isRefreshing) {
isRefreshing = true
;return refreshToken().then(newToken => {
store.commit(
'setToken', newToken); originalRequest.headers['Authorization'] = 'Bearer' + newToken; processQueue(null, newToken); return axios(originalRequest);}).catch(err => {
processQueue(err, null)
; store.commit('logout'); router.push('/login'); return Promise.reject(err);}).finally(() => {
isRefreshing = false
; });} else {
return new Promise((resolve, reject) => {
requests.
push((token) => {
originalRequest.headers[
'Authorization'] = 'Bearer' + token; resolve(axios(originalRequest)); }); });}
}
return Promise.reject(error)
;}
)
;function processQueue(error, token = null) {
requests.forEach(promise => {
if (error) {
promise.reject(error)
;} else {
promise.resolve(token)
;}
})
; requests = [];}

2. 实现刷新Token的方法

接下来,实现一个用于刷新Token的方法
refreshToken
。这个方法会调用后端接口来获取新的Token。

function refreshToken() {
return new Promise((resolve, reject) => {
axios.post(
'/auth/refresh', {refreshToken:store.state.refreshToken
}).then(response => {
if (response.data.success) {
resolve(response.data.token)
;} else {
reject(response.data.message)
;}
}).catch(error => {
reject(error)
; }); });}

3. 更新Vuex状态管理

确保在Vuex中有相关的状态和方法来管理Token和用户登录状态。

const store = new Vuex.Store({state:{token: localStorage.getItem('token') || '',refreshToken: localStorage.getItem('refreshToken') || '',user:{}
},
mutations:{
setToken(state, token) {
state.token = token
; localStorage.setItem('token', token);},
setRefreshToken(state, refreshToken) {
state.refreshToken = refreshToken
; localStorage.setItem('refreshToken', refreshToken);},
logout(state) {
state.token =
''; state.refreshToken = ''; state.user = {}; localStorage.removeItem('token'); localStorage.removeItem('refreshToken');}
}
})
;

4. 处理登录逻辑

确保在用户登录时,正确地存储Token和刷新Token。

function login(credentials) {
return axios.post(
'/auth/login', credentials).then(response => {
store.commit(
'setToken', response.data.token); store.commit('setRefreshToken', response.data.refreshToken); });}

总结:当用户在填写复杂表单时,即使Token过期也不会中断他们的操作,从而提供了更好的用户体验。这个机制不仅适用于表单填写,还可以广泛应用于任何需要长时间交互的Web应用场景中。

通过Jupyter Notebook+OpenAI+ollama简单的调用本地模型

起因是收到了ollama的邮件,貌似支持使用openai来调用本地的ollama下载的模型为自己用
image

想了下正好试下,因为这几天正好在尝试用Jupyter Notebook来写点调用api的方式来使用大语言模型,看看后面能不能自己做点有意思的操作,openai的api key懒得搞,整点阿里云的免费的token先用用还是可以的

刚好使用阿里的OpenAI Chat接口
兼容实例代码

要先做好的部分

打开ollama

直接cmd输入
ollama serve
,一般是没问题的

安装好openai包

在你要是用的环境下执行
pip install -U openai

我这里用的是1.36.1

image

确认没有问题以后,我们就使用
多轮对话
的部分,下面就是阿里云的实例代码

from openai import OpenAI
import os

def get_response(messages):
    client = OpenAI(
        # 如果您没有配置环境变量,请在此处用您的API Key进行替换
        api_key=os.getenv("DASHSCOPE_API_KEY"), 
        # 填写DashScope服务的base_url
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )
    completion = client.chat.completions.create(
        model="qwen-turbo",
        messages=messages,
        temperature=0.8,
        top_p=0.8
        )
    return completion

messages = [{'role': 'system', 'content': 'You are a helpful assistant.'}]
# 您可以自定义设置对话轮数,当前为3
for i in range(3):
    user_input = input("请输入:")
    # 将用户问题信息添加到messages列表中
    messages.append({'role': 'user', 'content': user_input})
    assistant_output = get_response(messages).choices[0].message.content
    # 将大模型的回复信息添加到messages列表中
    messages.append({'role': 'assistant', 'content': assistant_output})
    print(f'用户输入:{user_input}')
    print(f'模型输出:{assistant_output}')
    print('\n')

正常来说DASHSCOPE_API_KEY就是用阿里云的api key,我们按照邮件里面的说法,对其中的一些参数进行修改

修改后的

from openai import OpenAI
import os

def get_response(messages):
    client = OpenAI(
        # 如果您没有配置环境变量,请在此处用您的API Key进行替换
        api_key='ollama',
        # 填写DashScope服务的base_url
        base_url="http://localhost:11434/v1",
    )
    completion = client.chat.completions.create(
        model="qwen:14b",
        messages=messages,
        temperature=0.8,
        top_p=0.8
        )
    return completion

messages = [{'role': 'system', 'content': 'You are a helpful assistant.'}]
# 您可以自定义设置对话轮数,当前为3
for i in range(3):
    user_input = input("请输入:")
    # 将用户问题信息添加到messages列表中
    messages.append({'role': 'user', 'content': user_input})
    assistant_output = get_response(messages).choices[0].message.content
    # 将大模型的回复信息添加到messages列表中
    messages.append({'role': 'assistant', 'content': assistant_output})
    print(f'用户输入:{user_input}')
    print(f'模型输出:{assistant_output}')
    print('\n')

主要就是换这三个地方,api_key改成ollama,base_url改成本地的ollama端口我这里是http://localhost:11434/v1,model改成你的模型名,这里我用的是qwen:14b,没办法,电脑配置不好

image

然后就可以对话着玩了

image

如果调用成功了的话在cmd上会有输出输入,而且任务管理器的GPU会显著提升

image

前言

预计在 2024 年 11 月,C# 13 将与 .NET 9 一起正式发布。今年的 C# 更新主要集中在
ref struct
上进行了许多改进,并添加了许多有助于进一步提高生产力的便利功能。

本文将介绍预计将在 C# 13 中添加的功能。

注意:目前 C# 13 还未正式发布,因此以下内容可能会发生变化。

在迭代器和异步方法中使用
ref

ref struct

在使用 C# 进行编程时,你是否经常使用
ref
变量和
Span

ref struct
类型?然而,这些不能在迭代器和异步方法中使用,于是必须使用局部函数等来避免在迭代器和异步方法中直接使用
ref
变量
ref struct
类型,这非常不方便。

这个缺点在 C# 13 中得到了改善,现在迭代器和异步方法也可以使用
ref

ref struct
了!

在迭代器中使用
ref

ref struct
的例子:

IEnumerable<float> GetFloatNumberFromIntArray(int[] array)
{
    for (int i = 0; i < array.Length; i++)
    {
        Span<int> span = array.AsSpan();
        // 进行一些处理...
        ref float v = ref Unsafe.As<int, float>(ref array[i]);
        yield return v;
    }
}

在异步方法中使用
ref struct
的例子:

async Task ProcessDataAsync(int[] array)
{
    Span<int> span = array.AsSpan();
    // 进行一些处理...
    ref int element = ref span[42];
    element++;
    await Task.Yield();
}

为了展示功能,我使用了不适当且含糊不清的“一些处理”,不过重要的是现在可以使用
ref

ref struct
了!

但是,有一点需要注意,
ref
变量和
ref struct
类型的变量不能超出
yield

await
的边界使用。例如,以下示例将导致编译错误。

async Task ProcessDataAsync(int[] array)
{
    Span<int> span = array.AsSpan();
    // 进行一些处理...
    ref int element = ref span[42];
    element++;
    await Task.Yield();
    element++; // 错误:对 element 的访问超出了 await 的边界
}

虽然我们已经说到这里,但我想可能有人会疑惑,到底
ref

ref struct
是什么,所以我稍微解释一下。

在 C# 中,可以使用
ref
来获取变量的引用。这样,就可以通过引用来更改原始变量。以下是一个例子:

void Swap(ref int a, ref int b) // ref 表示引用
{
    int temp = a;
    a = b;
    b = temp; // 到这里,a 和 b 已经交换了
}

int x = 1;
int y = 2;
Swap(ref x, ref y); // 获取 x 和 y 的引用,调用 Swap 来交换 x 和 y

另一方面,
ref struct
是用于定义只能存在于堆栈上的值类型的。这是为了避免垃圾收集的开销。然而,由于
ref struct
只能存在于堆栈上,所以在 C# 13 之前,它不能在迭代器和异步方法等地方使用。

顺便一提,
ref struct
之所以带有
ref
,是因为
ref struct
的实例只能存在于堆栈上,其遵循的生命周期规则与
ref
变量相同。

allows ref struct
泛型约束

在以前,
ref struct
不能作为泛型类型参数使用,因此,考虑到代码的可重用性,引入了泛型,但最终
ref struct
不能使用,必须为
Span

ReadOnlySpan
重新编写相同的处理,于是就很麻烦。

在 C# 13 中,泛型类型也可以使用
ref struct
了:

using System;
using System.Numerics;

Process([1, 2, 3, 4], Sum); // 10
Process([1, 2, 3, 4], Multiply); // 24

T Process<T>(ReadOnlySpan<T> span, Func<ReadOnlySpan<T>, T> method)
{
    return method(span);
}

T Sum<T>(ReadOnlySpan<T> span) where T : INumberBase<T>
{
    T result = T.Zero;
    foreach (T value in span)
    {
        result += value;
    }
    return result;
}

T Multiply<T>(ReadOnlySpan<T> span) where T : INumberBase<T>
{
    T result = T.One;
    foreach (T value in span)
    {
        result *= value;
    }
    return result;
}

为什么像
ReadOnlySpan<T>
这样的
ref struct
类型可以作为
Func
的类型参数呢?为了调查这个问题,我查看了 .NET 的
源代码
,发现
Func
类型的泛型参数是这样定义的:

public delegate TResult Func<in T, out TResult>(T arg)
    where T : allows ref struct
    where TResult : allows ref struct;

如果在泛型参数上添加
allow ref struct
约束,那么就可以将
ref struct
类型传递给该参数。

这确实是一个方便的功能。

ref struct
也可以实现接口

在 C# 13 中,
ref struct
可以实现接口。

如果将此功能与
allows ref struct
结合使用,那么也可以通过泛型类型传递引用:

using System;
using System.Numerics;

int a = 10;
// 使用 Ref<int> 保存 a 的引用
Ref<int> aRef = new Ref<int>(ref a);
// 传递 Ref<int>
Increase<Ref<int>, int>(aRef);
Console.WriteLine(a); // 11

void Increase<T, U>(T data) where T : IRef<U>, allows ref struct where U : INumberBase<U>
{
    ref U value = ref data.GetRef();
    value++;
}

interface IRef<T>
{
    ref T GetRef();
}

// 为 Ref<T> 这样的 ref struct 实现接口
ref struct Ref<T> : IRef<T>
{
    private ref T _value;

    public Ref(ref T value)
    {
        _value = ref value;
    }

    public ref T GetRef()
    {
        return ref _value;
    }
}

这样一来,编写
ref struct
相关的代码就变得更容易了。另外,也能给各种
ref struct
实现的枚举器实现
IEnumerator
之类的接口了。

集合类型和
Span
也可以使用
params

在以前,
params
只能用于数组类型,但从 C# 13 开始,它也可以用于其他集合类型和
Span

params
是一种功能,允许在调用方法时直接指定任意数量的参数。

例如,

Test(1, 2, 3, 4, 5, 6);
void Test(params int[] values) { }

如上所示,可以直接指定任意数量的
int
参数。

从 C# 13 开始,除了数组类型外,其他集合类型、
Span

ReadOnlySpan
类型以及与集合相关的接口也可以添加
params

Test(1, 2, 3, 4, 5, 6);
void Test(params ReadOnlySpan<int> values) { }

// 或者
Test(1, 2, 3, 4, 5, 6);
void Test(params List<int> values) { }

// 接口也可以
Test(1, 2, 3, 4, 5, 6);
void Test(params IEnumerable<int> values) { }

这也很方便!

field
关键字

在实现 C# 的属性时,经常需要定义一大堆字段,如下所示...

partial class ViewModel : INotifyPropertyChanged
{
    // 定义字段
    private int _myProperty;

    public int MyProperty
    {
        get => _myProperty;
        set
        {
            if (_myProperty != value)
            {
                _myProperty = value;
                OnPropertyChanged();
            }
        }
    }
}

因此,从 C# 13 开始,
field
关键字将派上用场!

partial class ViewModel : INotifyPropertyChanged
{
    public int MyProperty
    {
        // 只需使用 field
        get => field;
        set
        {
            if (field != value)
            {
                field = value;
                OnPropertyChanged();
            }
        }
    }
}

不再需要自己定义字段,只需使用
field
关键字,字段就会自动生成。

这也非常方便!

部分属性

在编写 C# 时,常见的问题之一是:属性不能添加
partial
修饰符。

在 C# 中,可以在类或方法上添加
partial
,以便分别进行声明和实现。此外,还可以分散类的各个部分。它的主要用途是在使用源代码生成器等自动生成工具时,指定要生成的内容。

例如:

partial class ViewModel
{
    // 这里只声明方法,实现部分由工具自动生成
    partial void OnPropertyChanged(string propertyName);
}

然后自动生成工具会生成以下代码:

partial class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    partial void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new(propertyName));
    }
}

开发者只需要声明
OnPropertyChanged
,其实现将全部由自动生成,从而节省了开发者的时间。

从 C# 13 开始,属性也支持
partial

partial class ViewModel
{
    // 声明部分属性
    public partial int MyProperty { get; set; }
}

partial class ViewModel
{
    // 部分属性的实现
    public partial int MyProperty
    {
        get
        {
            // ...
        }
        set
        {
            // ...
        }
    }
}

这样,属性也可以由工具自动生成了。

锁对象

众所周知,
lock
是一种功能,通过监视器用于线程同步。

object lockObject = new object();
lock (lockObject)
{
    // 关键区
}

但是,这个功能的开销其实很大,会影响性能。

为了解决这个问题,C# 13 实现了锁对象。要使用此功能,只需用
System.Threading.Lock
替换被锁定的对象即可:

using System.Threading;

Lock lockObject = new Lock();
lock (lockObject)
{
    // 关键区
}

这样就可以轻松提高性能了。

初始化器中的尾部索引

索引运算符
^
可用于表示集合末尾的相对位置。从 C# 13 开始,初始化器也支持此功能:

var x = new Numbers
{
    Values = 
    {
        [1] = 111,
        [^1] = 999 // ^1 是从末尾开始的第一个元素
    }
    // x.Values[1] 是 111
    // x.Values[9] 是 999,因为 Values[9] 是最后一个元素
};

class Numbers
{
    public int[] Values { get; set; } = new int[10];
}

转义字符

在 Unicode 字符串中,可以使用
\e
代替
\u001b

\x1b

\u001b

\x1b

\e
都表示转义字符。它们通常用于表示控制字符。

  • \u001b
    表示 Unicode 转义序列,
    \u
    后面的 4 位十六进制数表示 Unicode 代码点
  • \x1b
    表示十六进制转义序列,
    \x
    后面的 2 位十六进制数表示 ASCII 代码
  • \e
    表示转义字符本身

推荐使用
\e
的原因是,可以避免在十六进制中的混淆。

例如,如果
\x1b
后面跟着
3
,则变为
\x1b3
,由于
\x1b

3
之间没有明确的分隔,因此不清楚应该分别解释成
\x1b

3
,还是放在一起解释。

如果使用
\e
,则可以避免混淆。

其他

除了上述功能外,方法组中的自然类型和方法重载中的优先级也有一些改进,但在本文中省略。如果想了解更多信息,请参阅文档。

结语

C# 正在年复一年地进化,对我来说 C# 13 的更新中实现了许多非常实用且方便的功能,解决了不少实际的痛点。期待 .NET 9 和 C# 13 的正式发布~