2024年2月

前言


vue3
开始
vue
引入了宏,比如
defineProps

defineEmits
等。我们每天写
vue
代码时都会使用到这些宏,但是你有没有思考过
vue
中的宏到底是什么?为什么这些宏不需要手动从
vue

import
?为什么只能在
setup
顶层中使用这些宏?

vue 文件如何渲染到浏览器上

要回答上面的问题,我们先来了解一下从一个
vue
文件到渲染到浏览器这一过程经历了什么?

我们的
vue
代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件。所以第一步就是通过
webpack
或者
vite
将一个vue文件编译为一个包含
render
函数的
js
文件。然后执行
render
函数生成虚拟DOM,再调用浏览器的
DOM API
根据虚拟DOM生成真实DOM挂载到浏览器上。

vue3的宏是什么?

我们先来看看
vue
官方的解释:

宏是一种特殊的代码,由编译器处理并转换为其他东西。它们实际上是一种更巧妙的字符串替换形式。

宏是在哪个阶段运行?

通过前面我们知道了
vue
文件渲染到浏览器上主要经历了两个阶段。

第一阶段是编译时,也就是从一个
vue
文件经过
webpack
或者
vite
编译变成包含render函数的js文件。此时的运行环境是
nodejs
环境,所以这个阶段可以调用
nodejs
相关的
api
,但是没有在浏览器环境内执行,所以不能调用浏览器的
API

第二阶段是运行时,此时浏览器会执行
js
文件中的
render
函数,然后依次生成虚拟
DOM
和真实
DOM
。此时的运行环境是浏览器环境内,所以可以调用浏览器的API,但是在这一阶段中是不能调用
nodejs
相关的
api

而宏就是作用于编译时,也就是从vue文件编译为js文件这一过程。

举个
defineProps
的例子:在编译时
defineProps
宏就会被转换为定义
props
相关的代码,当在浏览器运行时自然也就没有了
defineProps
宏相关的代码了。所以才说宏是在编译时执行的代码,而不是运行时执行的代码。

一个
defineProps
宏的例子

我们来看一个实际的例子,下面这个是我们的源代码:

<template>
  <div>content is {{ content }}</div>
  <div>title is {{ title }}</div>
</template>

<script setup lang="ts">
import {ref} from "vue"
const props = defineProps({
  content: String,
});
const title = ref("title")
</script>

在这个例子中我们使用
defineProps
宏定义了一个类型为
String
,属性名为
content

props
,并且在
template
中渲染
content
的内容。

我们接下来再看看编译成
js
文件后的代码,代码我已经进行过简化:

import { defineComponent as _defineComponent } from "vue";
import { ref } from "vue";

const __sfc__ = _defineComponent({
  props: {
    content: String,
  },
  setup(__props) {
    const props = __props;
    const title = ref("title");
    const __returned__ = { props, title };
    return __returned__;
  },
});

import {
  toDisplayString as _toDisplayString,
  createElementVNode as _createElementVNode,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

function render(_ctx, _cache, $props, $setup) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _createElementVNode(
          "div",
          null,
          "content is " + _toDisplayString($props.content),
          1 /* TEXT */
        ),
        _createElementVNode(
          "div",
          null,
          "title is " + _toDisplayString($setup.title),
          1 /* TEXT */
        ),
      ],
      64 /* STABLE_FRAGMENT */
    )
  );
}
__sfc__.render = render;
export default __sfc__;

我们可以看到编译后的
js
文件主要由两部分组成,第一部分为执行
defineComponent
函数生成一个
__sfc__
对象,第二部分为一个
render
函数。
render
函数不是我们这篇文章要讲的,我们主要来看看这个
__sfc__
对象。

看到
defineComponent
是不是觉得很眼熟,没错这个就是
vue
提供的API中的
definecomponent
函数。这个函数在运行时没有任何操作,仅用于提供类型推导。这个函数接收的第一个参数就是组件选项对象,返回值就是该组件本身。所以这个
__sfc__
对象就是我们的
vue
文件中的
script
代码经过编译后生成的对象,后面再通过
__sfc__.render = render

render
函数赋值到组件对象的
render
方法上面。

我们这里的组件选项对象经过编译后只有两个了,分别是
props
属性和
setup
方法。明显可以发现我们原本在
setup
里面使用的
defineProps
宏相关的代码不在了,并且多了一个
props
属性。没错这个
props
属性就是我们的
defineProps
宏生成的。

我们再来看一个不在
setup
顶层调用
defineProps
的例子:

<script setup lang="ts">
import {ref} from "vue"
const title = ref("title")

if (title.value) {
  const props = defineProps({
    content: String,
  });
}
</script>

运行这个例子会报错:
defineProps is not defined

我们来看看编译后的js代码:

import { defineComponent as _defineComponent } from "vue";
import { ref } from "vue";

const __sfc__ = _defineComponent({
  setup(__props) {
    const title = ref("title");
    if (title.value) {
      const props = defineProps({
        content: String,
      });
    }
    const __returned__ = { title };
    return __returned__;
  },
});

明显可以看到由于我们没有在
setup
的顶层调用
defineProps
宏,在编译时就不会将
defineProps
宏替换为定义
props
相关的代码,而是原封不动的输出回来。在运行时执行到这行代码后,由于我们没有任何地方定义了
defineProps
函数,所以就会报错
defineProps is not defined

总结

现在我们能够回答前面提的三个问题了。

  • vue
    中的宏到底是什么?

    vue3
    的宏是一种特殊的代码,在编译时会将这些特殊的代码转换为浏览器能够直接运行的指定代码,根据宏的功能不同,转换后的代码也不同。

  • 为什么这些宏不需要手动从
    vue

    import

    因为在编译时已经将这些宏替换为指定的浏览器能够直接运行的代码,在运行时已经不存在这些宏相关的代码,自然不需要从
    vue

    import

  • 为什么只能在
    setup
    顶层中使用这些宏?

    因为在编译时只会去处理
    setup
    顶层的宏,其他地方的宏会原封不动的输出回来。在运行时由于我们没有在任何地方定义这些宏,当代码执行到宏的时候当然就会报错。

如果想要在
vue
中使用更多的宏,可以使用
vue macros
。这个库是用于在vue中探索更多的宏和语法糖,作者是vue的团队成员
三咲智子

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!

技术背景

在前面的几篇博客中,我们介绍了
MindSponge分子动力学模拟框架的基本安装和使用

MindSponge执行分子动力学模拟任务的方法
。这里我们介绍一个在增强采样领域非常常用的工具:Collective Variable(CV),或者我们也可以直接称呼其为一个物理量。因为像化学反应或者是蛋白质折叠等问题中,经常会存在一个“路径(Path)”,使得反应沿着这个路径来进行。其中最简单的一种形式,就是成键断键。换句话说,我们可以通过调控这根键的键长,进而去调控这其中的化学反应,这也是分子力学层面的增强采样的一个基本思想。而随着增强采样技术的发展,越来越多的形式的CV被应用在不同的领域和问题当中。本文将会介绍,如何在基于深度学习框架MindSpore的分子动力学模拟软件MindSponge中,去定义一个CV。

能量极小化

首先我们使用到前面文章中提到过的MindSponge的能量极小化这个案例作为示例,优化的是这样的一个体系:

对应的pdb文件为:

REMARK   Generated By Xponge (Molecule)
ATOM      1    N ALA     1      -0.095 -11.436  -0.780
ATOM      2   CA ALA     1      -0.171 -10.015  -0.507
ATOM      3   CB ALA     1       1.201  -9.359  -0.628
ATOM      4    C ALA     1      -1.107  -9.319  -1.485
ATOM      5    O ALA     1      -1.682  -9.960  -2.362
ATOM      6    N ARG     2      -1.303  -8.037  -1.397
ATOM      7   CA ARG     2      -2.194  -7.375  -2.328
ATOM      8   CB ARG     2      -3.606  -7.943  -2.235
ATOM      9   CG ARG     2      -4.510  -7.221  -3.228
ATOM     10   CD ARG     2      -5.923  -7.789  -3.136
ATOM     11   NE ARG     2      -6.831  -7.666666  -4.087
ATOM     12   CZ ARG     2      -8.119  -7.421  -4.205
ATOM     13  NH1 ARG     2      -8.686  -8.371  -3.468
ATOM     14  NH2 ARG     2      -8.844  -6.747  -5.093
ATOM     15    C ARG     2      -2.273  -5.882  -2.042
ATOM     16    O ARG     2      -1.630  -5.388  -1.119
ATOM     17    N ALA     3      -3.027  -5.119  -2.777
ATOM     18   CA ALA     3      -3.103  -3.697  -2.505
ATOM     19   CB ALA     3      -1.731  -3.041  -2.625
ATOM     20    C ALA     3      -4.039  -3.001  -3.483
ATOM     21    O ALA     3      -4.614  -3.643  -4.359
ATOM     22    N ALA     4      -4.235  -1.719  -3.394
ATOM     23   CA ALA     4      -5.126  -1.057  -4.325
ATOM     24   CB ALA     4      -6.538  -1.625  -4.233
ATOM     25    C ALA     4      -5.205   0.436  -4.039
ATOM     26    O ALA     4      -4.561   0.930  -3.116
ATOM     27  OXT ALA     4      -5.915   1.166  -4.728
TER

那么我们就可以通过调控体系中每个原子的坐标,来使其能量达到一个最低值。相关代码如下所示,这里我们用的是一个MindSpore自带的Adam优化器:

from mindspore import nn, context
from sponge import ForceField, Sponge, set_global_units, Protein
from sponge.callback import RunInfo, WriteH5MD
 
# 配置MindSpore的执行环境
context.set_context(mode=context.GRAPH_MODE, device_target='GPU', device_id=1)
# 配置全局单位
set_global_units('A', 'kcal/mol')
 
# 定义一个基于case1.pdb的分子系统
system = Protein('case1.pdb', template=['protein0.yaml'], rebuild_hydrogen=True)
# 定义一个amber.ff99sb的力场
energy = ForceField(system, parameters=['AMBER.FF99SB'])
# 定义一个学习率为1e-03的Adam优化器
min_opt = nn.Adam(system.trainable_params(), 1e-03)
 
# 定义一个用于执行分子模拟的Sponge实例
md = Sponge(system, potential=energy, optimizer=min_opt)
 
# RunInfo这个回调函数可以在屏幕上根据指定频次输出能量参数
run_info = RunInfo(200)
# WriteH5MD回调函数,可以将轨迹、能量、力和速度等参数保留到一个hdf5文件中,文件后缀为h5md
cb_h5md = WriteH5MD(system, 'test.h5md', save_freq=10, write_image=False)
# 开始执行分子动力学模拟,运行2000次迭代
md.run(2000, callbacks=[run_info, cb_h5md])

运行结果是这样的:

[MindSPONGE] Adding 45 hydrogen atoms for the protein molecule in 0.003 seconds.
[MindSPONGE] Started simulation at 2023-09-04 15:14:29
[MindSPONGE] Step: 0, E_pot: 1200.4639
[MindSPONGE] Step: 200, E_pot: 7.763489
[MindSPONGE] Step: 400, E_pot: -70.34643
[MindSPONGE] Step: 600, E_pot: -96.88522
[MindSPONGE] Step: 800, E_pot: -109.98717
[MindSPONGE] Step: 1000, E_pot: -117.33747
[MindSPONGE] Step: 1200, E_pot: -121.95378
[MindSPONGE] Step: 1400, E_pot: -125.20764
[MindSPONGE] Step: 1600, E_pot: -127.72044
[MindSPONGE] Step: 1800, E_pot: -129.79828
[MindSPONGE] Finished simulation at 2023-09-04 15:15:16
[MindSPONGE] Simulation time: 46.79 seconds.
--------------------------------------------------------------------------------

除了在屏幕上输出每一步的能量,我们还通过
WriteH5MD
将整个轨迹写到了一个h5md的文件中,然后可以用silx这样的工具来简单的对h5md文件进行分析和可视化:

定义Collective Variable

我们在这个体系分子中随便摘取两个原子
\(i\)

\(j\)
作为计算CV的参量,首先我们使用一个
距离
作为CV:

\[V=|\textbf{r}_j-\textbf{r}_i|
\]

如果用VMD来查看这个pdb文件,那么我们定义的CV就是如下图所示的两个原子之间的距离:

在代码侧,我们可以直接从
sponge.colvar
中调用包装好的
Distance
类来定义这个距离的CV。然后作为一个
metrics
传入到
Sponge
对象中,就可以对这个CV的值进行跟踪,作为轨迹的一部分。当然,其实MindSponge也是可以支持使用这个CV去计算一个Bias Energy偏置势的,这就涉及到不同的增强采样算法的不同定义,如Meta Dynamics等,这里暂不做介绍。另外我们还可以基于
sponge.colvar
中的
Colvar
基础类去定义一个我们自己所需要的CV,并传入到metrics中,具体定义形式可以参考
Distance
的写法。

from mindspore import nn, context
import sys
sys.path.insert(0, '../..')
from sponge import ForceField, Sponge, set_global_units, Protein
from sponge.callback import RunInfo, WriteH5MD
from sponge.colvar import Distance

# 配置MindSpore的执行环境
context.set_context(mode=context.GRAPH_MODE, device_target='GPU', device_id=1)
# 配置全局单位
set_global_units('A', 'kcal/mol')

# 定义一个基于case1.pdb的分子系统
system = Protein('../pdb/case1.pdb', template=['protein0.yaml'], rebuild_hydrogen=True)
# 定义一个amber.ff99sb的力场
energy = ForceField(system, parameters=['AMBER.FF99SB'])
# 定义一个学习率为1e-03的Adam优化器
min_opt = nn.Adam(system.trainable_params(), 1e-03)

# 定义CV
bond0 = Distance([0, 1])
print(system.atom_name)
# 定义一个用于执行分子模拟的Sponge实例
md = Sponge(system, potential=energy, optimizer=min_opt, metrics={'bond0': bond0})

# RunInfo这个回调函数可以在屏幕上根据指定频次输出能量参数
run_info = RunInfo(200)
# WriteH5MD回调函数,可以将轨迹、能量、力和速度等参数保留到一个hdf5文件中,文件后缀为h5md
cb_h5md = WriteH5MD(system, 'test.h5md', save_freq=10, write_image=False)
# 开始执行分子动力学模拟,运行2000次迭代
md.run(2000, callbacks=[run_info, cb_h5md])

运行输出如下所示:

[MindSPONGE] Adding 45 hydrogen atoms for the protein molecule in 0.003 seconds.
[['N' 'CA' 'CB' 'C' 'O' 'H1' 'H2' 'H3' 'HA' 'HB1' 'HB2' 'HB3' 'N' 'CA'
  'CB' 'CG' 'CD' 'NE' 'CZ' 'NH1' 'NH2' 'C' 'O' 'H' 'HA' 'HB2' 'HB3' 'HG2'
  'HG3' 'HD2' 'HD3' 'HE' 'HH11' 'HH12' 'HH21' 'HH22' 'N' 'CA' 'CB' 'C'
  'O' 'H' 'HA' 'HB1' 'HB2' 'HB3' 'N' 'CA' 'CB' 'C' 'O' 'OXT' 'H' 'HA'
  'HB1' 'HB2' 'HB3']]
[MindSPONGE] Started simulation at 2024-02-19 15:33:57
[MindSPONGE] Step: 200, E_pot: 8.525536, bond0: 1.4885269
[MindSPONGE] Step: 400, E_pot: -70.148384, bond0: 1.4816087
[MindSPONGE] Step: 600, E_pot: -96.796265, bond0: 1.4798437
[MindSPONGE] Step: 800, E_pot: -109.93948, bond0: 1.4798721
[MindSPONGE] Step: 1000, E_pot: -117.30916, bond0: 1.4806045
[MindSPONGE] Step: 1200, E_pot: -121.934845, bond0: 1.4812899
[MindSPONGE] Step: 1400, E_pot: -125.193565, bond0: 1.4817244
[MindSPONGE] Step: 1600, E_pot: -127.70915, bond0: 1.4819595
[MindSPONGE] Step: 1800, E_pot: -129.78864, bond0: 1.482085
[MindSPONGE] Step: 2000, E_pot: -131.60873, bond0: 1.4821533
[MindSPONGE] Finished simulation at 2024-02-19 15:34:44
[MindSPONGE] Simulation time: 47.89 seconds.
--------------------------------------------------------------------------------

可以看到,相比于之前纯粹的能量极小化版本代码,这里我们多打印了一个
bond0
的参数,这里就是每一步优化能量之后对应的CV的值。类似的,我们还可以用
Angle

Torsion
等MindSponge内置的CV类型去计算:

from mindspore import nn, context
import sys
sys.path.insert(0, '../..')
from sponge import ForceField, Sponge, set_global_units, Protein
from sponge.callback import RunInfo, WriteH5MD
from sponge.colvar import Distance, Angle, Torsion

# 配置MindSpore的执行环境
context.set_context(mode=context.GRAPH_MODE, device_target='GPU', device_id=1)
# 配置全局单位
set_global_units('A', 'kcal/mol')

# 定义一个基于case1.pdb的分子系统
system = Protein('../pdb/case1.pdb', template=['protein0.yaml'], rebuild_hydrogen=True)
# 定义一个amber.ff99sb的力场
energy = ForceField(system, parameters=['AMBER.FF99SB'])
# 定义一个学习率为1e-03的Adam优化器
min_opt = nn.Adam(system.trainable_params(), 1e-03)

# 定义CV
cv_bond = Distance([0, 1])
cv_angle = Angle([0, 1, 2])
cv_dihedral = Torsion([0, 1, 2, 3])
# 定义一个用于执行分子模拟的Sponge实例
md = Sponge(system, potential=energy, optimizer=min_opt, metrics={'bond': cv_bond, 'angle': cv_angle,
                                                                  'dihedral': cv_dihedral})

# RunInfo这个回调函数可以在屏幕上根据指定频次输出能量参数
run_info = RunInfo(200)
# WriteH5MD回调函数,可以将轨迹、能量、力和速度等参数保留到一个hdf5文件中,文件后缀为h5md
cb_h5md = WriteH5MD(system, 'test.h5md', save_freq=10, write_image=False)
# 开始执行分子动力学模拟,运行2000次迭代
md.run(2000, callbacks=[run_info, cb_h5md])

运行输出如下所示:

[MindSPONGE] Adding 45 hydrogen atoms for the protein molecule in 0.004 seconds.
[MindSPONGE] Started simulation at 2024-02-19 15:43:11
[MindSPONGE] Step: 200, E_pot: 8.52552, bond: 1.4885279, angle: 1.9996612, dihedral: 2.14814
[MindSPONGE] Step: 400, E_pot: -70.14837, bond: 1.4816078, angle: 1.9796473, dihedral: 2.1302822
[MindSPONGE] Step: 600, E_pot: -96.796265, bond: 1.4798437, angle: 1.9628391, dihedral: 2.1359558
[MindSPONGE] Step: 800, E_pot: -109.939514, bond: 1.4798703, angle: 1.9526237, dihedral: 2.1443036
[MindSPONGE] Step: 1000, E_pot: -117.30916, bond: 1.4806036, angle: 1.9461793, dihedral: 2.1488183
[MindSPONGE] Step: 1200, E_pot: -121.93486, bond: 1.4812908, angle: 1.9418821, dihedral: 2.1499696
[MindSPONGE] Step: 1400, E_pot: -125.19357, bond: 1.4817244, angle: 1.9388888, dihedral: 2.1492646
[MindSPONGE] Step: 1600, E_pot: -127.70906, bond: 1.4819595, angle: 1.9367135, dihedral: 2.1476927
[MindSPONGE] Step: 1800, E_pot: -129.78867, bond: 1.4820842, angle: 1.9350405, dihedral: 2.1457095
[MindSPONGE] Step: 2000, E_pot: -131.60872, bond: 1.4821533, angle: 1.9336755, dihedral: 2.143513
[MindSPONGE] Finished simulation at 2024-02-19 15:44:03
[MindSPONGE] Simulation time: 51.27 seconds.
--------------------------------------------------------------------------------

保存轨迹

在上一步运行之后,会在本地生成一个h5md文件,我们可以用silx view查看这个文件。此时我们会发现,在代码中定义的metrics中几个CV物理量,也会被同步保存到h5md轨迹文件中:

总结概要

随着分子动力学模拟技术的应用推广、AI软件的发展和硬件算力水平的提升,我们可以更快的在分子层面去观察和研究分子体系内的相互作用。但是分子模拟的性能再好,也不一定可以复现一些在自然宏观状态下有可能发生的化学反应或者是物质相变。因此我们需要通过定义一些对反应路径有决定性影响的物理量,然后结合增强采样技术,去更快的复现和推导我们所需要的反应机理。本文主要介绍分子动力学模拟软件MindSponge在这一领域的应用和代码实现。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/cv.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

参考链接

  1. https://www.mindspore.cn/mindsponge/docs/zh-CN/r1.0.0-rc2/user/simulation.html

搜索引擎对互联网的重要性不言而喻,不过,随着
ChatGPT
及其类似AI工具的推出,对搜索引擎带来了前所未有的挑战。

因为
ChatGPT
具有自然语言处理能力,能够更好地理解用户的搜索意图,提供更准确、更相关的搜索结果。
同时,还可以根据用户的搜索历史和行为数据,为用户提供更加个性化的搜索体验,推荐更符合用户需求的内容。

不过,目前
ChatGPT
并不能完全替代传统搜索引擎。
传统搜索引擎在信息索引、查询准确度和查询功能等方面仍具有优势。

本票介绍Google搜索引擎中增强搜索技巧的一些搜索运算符,
看看传统搜索引擎的在准确性,效率,结果多样性和稳定性方面依然强大的优势。

1. 限定范围搜索

1.1. 搜索结果中必须包含指定内容

搜索时,给搜索的关键字加上双引号
""
,可以保证搜索内容中完整的包含搜索关键字,
并且在搜索结果列表中高亮显示你的搜索关键字。
b9495efc5649937ea4188e50a6d14ee.png
搜索关键字是
python实现递归算法

左边
是不加双引号,
右边
是加双引号的效果。

1.2. 搜索定义

搜索某个关键字的定义时,使用
define:
运算符。
image.png

1.3. 指定在某个网站搜索

通过
site:
运算符,指定搜索某个网站,比如下面只搜索
博客园
中关于
量子力学
的内容。
image.png

1.4. 标题包含特定词汇

使用运算符
intitle:
,指定标题中包含某个特定词汇。
image.png

如果需要包含多个关键字,使用
allintitle:
运算符。
image.png

1.5. 内容包含特定词汇

使用方法和上面的
intitle:
类似。

intext:
指定网页内容中包含某个特定词汇;用
allintext:
指定网页内容中包含的多个特定词汇。

2. 组合条件搜索

当使用多个关键字搜索时,可以用布尔运算符来组合条件之间的关系。

2.1. AND运算符

AND
运算符表示搜索结果中包含所有关键字。
image.png

2.2. OR 运算符

OR
运算符表示搜索结果中包含所有关键字之一。
image.png

2.3. - 运算符

-
运算符表示搜索结果不要包含输入的关键字。
下面的搜索表示,搜索包含
vue
,但是不包含
react
的内容。
image.png

3. 指定文件类型搜索

指定文件类型使用运算符
filetype:
或者
ext:
,这两个随便用哪个,效果都一样。
image.png
点击上面的搜索结果,可以发现,前面的几个都是
PDF文件

4. 限定时间搜索

限定时间检索当我们查找某个时期特定资料的时候特别有用。

4.1. 某个时间之前

使用
before:
运算符。
image.png
可以看到检索结果的时间,确实在 **2021年1月1日 **之前。

4.2. 某个时间之后

使用
after:
运算符。
image.png
可以看到检索结果的时间,确实在 **2023年1月1日 **之后。

5. 功能类搜索

gogole搜索
还可以当成小工具来使用,比如:

5.1. 天气

通过
weather:
来查询某个城市的天气情况。
image.png

5.2. 股票

通过
stocks:
来查询某个股票的信息,需要知道股票的代码。
image.png

5.3. 地图

通过
map:
关键字可以很方便的在地图上显示某个位置。
image.png
点击检索结果中的图片,就能迁移到
google map
网站中。

5.4. 电影

还可以用
movie:
来搜索某个关键字关联的电影。
image.png
《模仿游戏》就是一部关于图灵的知名电影。

5.5. 单位转换

通过
in
运算符,可以很方便的查询不同单位之间的转换。
比如,查询汇率:
image.png

再比如,长度单位转换:
image.png

6. 总结

google
提供的这些搜索运算符非常强大,使用起来也容易。
上面只是单独介绍各个运算符的使用方式,其实它们都是可以任意组合起来使用的,
平时大家可以多组合使用试试,如果发现好的高效的组合方式,也欢迎分享出来。

数据类型介绍

Go语言中的数据类型分为:基本数据类型和复合数据类型

  • 基本数据类型:整型、浮点型、布尔型、字符串

  • 复合数据类型:数组、切片、结构体、函数、map、通道(channel)、接口等

基本数据类型:

整型

整型分为两个大类:

  • 有符号整型按长度分为:int8、int16、int32、int64
  • 对应的无符号整型:uint8、uint16、uint32、uint64
  • image-20240218175158230

特殊整型:

image-20240218180702269

// 定义int类型
var num int = 10

var number int  // 默认值为0

// 转换为 int32
var b  = int32(num)


浮点型

Go语音支持两种浮点型,数据格式遵循IEEE 754标准

  • float32:最大的范围约为-3.4e38~3.4e38,可以使用math.MaxFloat32定义

  • float64:最大的范围约为-1.8e308~1.8e308,可以使用math.MaxFloat64定义

  • import (
    	"fmt"
    	"math"
    )
    
    func main() {
    
    	fmt.Println(-math.MaxFloat32)
    	fmt.Println(math.MaxFloat32)
    	fmt.Println(-math.MaxFloat64)
    	fmt.Println(math.MaxFloat64)
    
    }
    
    

定义浮点型

func main() {

	var a float32 = 3.12 // 占用4个字节
	var b float64 = 5.12 // 占用8个字节
  var c float // 默认值为0

	fmt.Println(a)
	fmt.Println(b)
}

格式化输出

func main() {
	var a float32 = 3.123123623
	fmt.Printf("%f", a)   // 默认保留6位小数且四舍五入,3.123124
	fmt.Printf("%.2f", a) // 默认保留2位小数且四舍五入 3.12
  


	var fn float32 = 3.14e2  // 用科学技术法表示浮点型
	fmt.Printf("%f", fn) // 314.000000




}

默认浮点类型

func main() {
 // 在32位系统中,没有显示声明,则默认是float32,在64位系统中,就是float64
	a := 3.1233123
	fmt.Printf("%T", a)


浮点型精度丢失问题

image-20240218184034616

可以使用第三方包来解决精度损失问题->:
https://github.com/shopspring/decimal

布尔型
  • 布尔类型变量的默认值为false

  • Go语言中不允许将整型强制转换为布尔型

  • 布尔型无法参与数值运算,也无法与其他类型进行转换

var flag bool = true
var status bool // 默认值为false
字符串

声明字符串类型:

func main() {
	var work string // 默认值为空
	var name string = "木子"  // 显示的声明字符串类型
	var car = "梅赛德斯奔驰AMG GLE 53 Couple" // 根据表达式推导变量的类型
	city := "Beijing" // 根据表达式推导变量的类型
	fmt.Println(work)
	fmt.Println(name)
	fmt.Println(car)
	fmt.Println(city)

}

字符串转义符

通常使用/来进行字符串的特殊符号转义

func main() {
	// \n 表示换行
	str1 := "this \nis str"
	// C:\Go\bin 输出反斜杠
	str2 := "C:\\Go\\bin"

	// C:Go"bin 输出“
	str3 := "C:Go\"bin"

	fmt.Println(str1)
	fmt.Println(str2)
	fmt.Println(str3)

}

输出多行字符串

使用双引号
""
只能输出一行字符串,不能输出多行,如果要输出多行,使用反引号

``

func main() {
	str1 := `
   第一行内容 
   第二行内容
`
	fmt.Println(str1)
}

字符串的常用方法

import (
	"fmt"
	"strings"
)

func main() {
	var str1 = "this is str"
	// 输出字符串的长度
	fmt.Println(len(str1))
	// 拼接字符串 使用 + 号
	str2 := "你好"
	fmt.Println(str2 + str1)

	// 拼接字符串 使用fmt.Sprintf,fmt.Sprintf的作用是格式化字符串赋值给新串
	str3 := fmt.Sprintf("%v %v", str2, str1)
	fmt.Println(str3)

	// strings.Split,分割字符串,需要引入strings包,Split方法第一个参数是要分割的字符串,第二个参数是以什么进行分割
	var str4 = "123-456-789"
	arr := strings.Split(str4, "-")
	fmt.Println(arr) // [123 456 789] 切片数组

	// strings.Join(),把一个切片连接成一个字符串 第一个参数是要连接的数组,第二个参数是以什么连接
	str5 := strings.Join(arr, "*")
	fmt.Println(str5) // 123*456*789

	// strings.Contains(),判断一个字符串是否包含另一个字符串,包含返回true,否则false
	str6 := "this is str"
	str7 := "this"
	flag := strings.Contains(str6, str7)
	fmt.Println(flag) // true

	// 	strings.HasPrefix() / 	strings.HasSuffix() 字符串前缀、后缀判断
	str8 := "this is str"
	str9 := "this"
	// 判断 str8的前缀是不是str9,是则是true,否则false
	first := strings.HasPrefix(str8, str9)
	fmt.Println(first)
	// 判断str8的后缀是不是str,是则是true,否则false
	last := strings.HasSuffix(str8, "str")
	fmt.Println(last)

	//	strings.Index() / 	strings.LastIndex() 子串出现的位置
	// 子串在str8中出现的位置,从前往后查找 查找不到返回-1
	indexNum := strings.Index(str8, "is")
	fmt.Println(indexNum) // 2
	// 子串在str8中出现的位置,从后往前查找 查找不到返回-1
	lastNum := strings.LastIndex(str8, "s")
	fmt.Println(lastNum) // 8

}

byte和rune类型

组成每个字符串的元素叫做字符,可以通过遍历字符串元素获得字符,字符用单引号``包裹起来

Go语言的字符有两种:

  • uint8类型,就是byte型,代表了ascii码的一个字符

  • rune类型,代表一个utf-8字符

  • 当需要处理中文、日文或者其他复合字符时,则需要用到rune类型,rune类型实际是一个int32

  • 字符串中的单个字符,一个汉字占用3个字节(utf-8),一个字母占用一个字节,使用len方法的长度根据类型获取

  • Go 使用了 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理

func main() {
	// golang中定义字符,字符属于int类型,对应ascii码数值
	a := 'a'
	b := '0'
	// 当我们直接输出字符的时候,输出的是这个字符对应的ascii码的数值
	fmt.Println(a) // 97
	fmt.Println(b) // 48

	// 原样输出字符 使用%c格式化输出 , 相应 Unicode 码点所表示的字符
	fmt.Printf("%c", a) // a

}

修改字符串

要修改字符串,需要先将其转换成[]rune或者[]byte,完成后再转回为string,无论哪种转回,都会重新分配内存,并赋值字节数组

func main() {
	s1 := "big"
	// 1.将字符串s1 转换为byte类型
	byteStr := []byte(s1)

	// 2.将byte类型的第一个字符修改为p
	byteStr[0] = 'p'
	// 3. 将修改后的byte转回string类型
	fmt.Println(string(byteStr)) // pig

	// 如果字符串中有中文,转换为则需要为rune类型
	s2 := "你好golang"
	// 1. 将s2转为rune类型
	runeStr2 := []rune(s2)
	// 2. 将转换后的第一个字符修改
	runeStr2[0] = '李'
	// 3. 将修改后的字符转回string类型
	fmt.Println(string(runeStr2))

}

数值类型之间的转换
import (
	"fmt"
	"strconv"
)

func main() {

	/*

		数值类型的转换 int、float建议从低位转换成高位,如果是高位转换低位可能会溢出
		不同的数据类型不可以进行比较、运算,会报类型错误

	*/

	// int类型转换
	var int1 int8 = 10
	var int2 int16 = 20
	fmt.Println(int16(int1) + int2)

	// 整型和浮点型转换
	var float1 float32 = 20.312
	fmt.Println(float32(int2) + float1)

	/* 字符串转换方式一:通过Sprintf将其他类型转换为string */

	var t bool = true
	var b byte = '7'
	// int类型转换string
	str1 := fmt.Sprintf("%d", int1)
	fmt.Println(str1)
	// float类型转换string
	str2 := fmt.Sprintf("%f", float1)
	fmt.Println(str2)
	// bool转换为string
	str3 := fmt.Sprintf("%t", t)
	fmt.Println(str3)
	// 字符转换为string
	str4 := fmt.Sprintf("%c", b)
	fmt.Println(str4)

	/* 字符串转换方式二: 使用strconv,需要import strconv包*/
	var i1 int = 20
	// 数字转换字符串
	s1 := strconv.FormatInt(int64(i1), 10) // 第一个参数是int64的数值,第二个参数是int类型的进制
	fmt.Println(s1)

	// 浮点转字符串
	var f1 float32 = 20.23
	s2 := strconv.FormatFloat(float64(f1), 'f', 2, 64) // Param:要转换的值、格式化类型、保留的位数、64位or32位
	fmt.Println(s2)

	// bool转换字符串
	s3 := strconv.FormatBool(t)
	fmt.Println(s3)

	// 字符转换字符串
	s4 := strconv.FormatUint(uint64(b), 10) // Param:unit64的数值、输出的进制
	fmt.Println(s4)

	/* string类型转换为数值型*/

	var string1 string = "10"
	// string 转换为int 返回值有两个,结果和错误
	num1, _ := strconv.ParseInt(string1, 10, 64) // Param:字符串、进制、位数
	fmt.Println(num1)

	// string转换为float
	var string2 string = "3.14"
	float2, _ := strconv.ParseFloat(string2, 64) // Param:字符串、位数
	fmt.Println(float2)

}

针对“缓冲区”编程是一个非常注重“性能”的地方,我们应该尽可能地避免武断地创建字节数组来存储读取的内容,这样不但会导致大量的字节拷贝,临时创建的字节数组还会带来GC压力。要正确、高效地读写缓冲内容,我们应该对几个我们可能熟悉的类型具有更深的认识。

一、Array、ArraySegment、Span<T>、Memory<T>与String
二、MemoryManager<T>
三、ReadOnlySequence<T>
四、创建“多段式”ReadOnlySequence<T>
五、高效读取ReadOnlySequence<T>

一、Array、ArraySegment、Span<T>、Memory<T>与String

Array、ArraySegment、Span<T>、Memory<T>,以及ReadOnlySpan<T>与ReadOnlyMemory<T>本质上都映射一段连续的内存,但是它们又有些差异,导致它们具有各自不同的应用场景。Array是一个类(引用类型),所以一个Array对象是一个托管对象,其映射的是一段托管堆内存。正因为Array是一个托管对象,所以它在托管堆中严格遵循“三段式(Object Header + TypeHandle + Payload)”内存布局,Payload部分包含前置的长度和所有的数组元素(数组的内存布局可以参阅我的文章《
.NET中的数组在内存中如何布局?
》),其生命周期受GC管理。

顾名思义,ArraySegment代表一个Array的“切片”,它利用如下所示的三个字段(_array、_offset和count)引用数组的一段连续的元素。由于Array是托管对象,所以ArraySegment映射的自然也只能是一段连续的托管内存。由于它是只读结构体(值类型),对GC无压力,在作为方法参数时按照“拷贝”传递。

public readonly struct ArraySegment<T>
{
    private readonly T[] _array;
    private readonly int _offset;
    private readonly int _count;
    public T[]? Array => _array;
    public int Offset => _offset;
    public int Count => _count;

}

不同于ArraySegment,一个Span<T>不仅仅可以映射一段连续的托管内存,还可以映射一段连续的非托管内存;不仅可以映射一段堆内存,还能映射一段栈内存(比如Span<byte> buffer = stackalloc byte[8]),这一点可以从它定义的构造函数看出来。

public readonly ref struct Span<T>
{
    public Span(T[]? array);
    public Span(T[]? array, int start, int length);
    public unsafe Span(void* pointer, int length);
    public Span(ref T reference);
    internal Span(ref T reference, int length);
}

由于Span<T>是一个只读引用结构体,意味着它总是以引用的方式被使用,换言之当我们使用它作为参数传递时,传递的总是这个变量自身的栈地址。正因为如此,在某个方法中创建的Span<T>只能在当前方法执行范围中被消费,如果“逃逸”出这个范围,方法对应的栈内存会被回收。所以和其他引用结构体一样,具有很多的使用上限制(可以参阅我的文章《
除了参数,ref关键字还可以用在什么地方?
》),所以我们才有了Memory<T>。

由于Memory<T>就是一个普通的只读结构体,所以在使用上没有任何限制。但是也正因为如此,它只能映射一段连续的托管堆内存和非托管内存,不能映射栈内存。从如下所示的构造函数可以看出,我们可以根据一个数组对象的切片创建一个Memory<T>,此时它相当于一个ArraySegment<T>,针对非托管内存的映射需要是借助一个MemoryManager<T>对象来实现的。

public readonly struct Memory<T>
{
    public Memory(T[]? array);
internal Memory(T[] array, int start); public Memory(T[]? array, int start, int length); internal Memory(MemoryManager<T> manager, int length); internal Memory(MemoryManager<T> manager, int start, int length); }

Span<T>和Memory<T>虽然自身是自读结构体,但是它Cover的“片段”并不是只读的,我们可以在对应的位置写入相应的内容。在只读的场景中,我们一般会使用它们的只读版本ReadOnlySpan<T>和ReadOnlySpanMemory<T>。除了这些,我们还会经常使用另一种类型的“连续内存片段”,那就是字符串,其内存布局可以参阅《
你知道.NET的字符串在内存中是如何存储的吗?

二、MemoryManager<T>

从上面给出的Memory<T>构造函数可以看出,一个Memory<T>可以根据一个MemoryManager<T>来创建的。MemoryManager<T>是一个抽象类,从其命名可以看出,它用来“管理一段内存”。具体它可以实施怎样的内存管理功能呢?我们先从它实现的两个接口开始说起。

MemoryManager<T>实现的第一个接口为如下这个IMemoryOwner<T> ,顾名思义,它代表某个Memory<T>对象(对应Memory属性)的持有者,我们用它来管理Memory<T>对象的生命周期。比如表示内存池的MemoryPool<T>返回的就是一个IMemoryOwner<T>对象,我们利用该对象得到从内存池中“借出”的Memory<T>对象,如果不再需要,直接调用IMemoryOwner<T>对象的Dispose方法将其“归还”到池中。

public interface IMemoryOwner<T> : IDisposable
{
    Memory<T> Memory { get; }
}

托管对象可以以内存地址的形式进行操作,但前提是托管对象在内存中的地址不会改变,但是我们知道GC在进行压缩的时候是会对托管对象进行移动,所以我们需要固定托管内存的地址。MemoryManager<T>实现了第二个接口IPinnable提供了两个方法,指定元素对象内存地址的固定通过Pin方法来完成,该方法返回一个MemoryHandle对象,后者利用封装的GCHandle句柄来持有执行指针指向的内存。另一个方法Unpin用来解除内存固定。

public interface IPinnable
{
    MemoryHandle Pin(int elementIndex);
    void Unpin();
}

public struct MemoryHandle : IDisposable
{
    private unsafe void* _pointer;
    private GCHandle _handle;
    private IPinnable _pinnable;

    [CLSCompliant(false)]
    public unsafe void* Pointer => _pointer;

    [CLSCompliant(false)]
    public unsafe MemoryHandle(void* pointer, GCHandle handle = default(GCHandle), IPinnable? pinnable = null)
    {
        _pointer = pointer;
        _handle = handle;
        _pinnable = pinnable;
    }

    public unsafe void Dispose()
    {
        if (_handle.IsAllocated)
        {
            _handle.Free();
        }
        if (_pinnable != null)
        {
            _pinnable.Unpin();
            _pinnable = null;
        }
        _pointer = null;
    }
}

抽象类MemoryManager<T>定义如下。它提供了一个抽象方法GetSpan,并利用它返回的Span<T>来创建Memory属性返回的Memory<T>。针对IPinnable接口的两个方法Pin和Unpin体现为两个抽象方法。

public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable
{
    public virtual Memory<T> Memory => new(this, GetSpan().Length);
    public abstract Span<T> GetSpan();
    public abstract MemoryHandle Pin(int elementIndex = 0);
    public abstract void Unpin();

    protected Memory<T> CreateMemory(int length) => new(this, length);
    protected Memory<T> CreateMemory(int start, int length)=> new(this, start, length);

    protected internal virtual bool TryGetArray(out ArraySegment<T> segment)
    {
        segment = default;
        return false;
    }

    void IDisposable.Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    protected abstract void Dispose(bool disposing);
}

如果我们需要创建了针对非托管内存的Memory<T>,可以按照如下的形式自定义一个MemoryManager<T>派生类UnmanagedMemoryManager<T>,然后根据这样一个对象创建Memory<T>对象即可。

public sealed unsafe class UnmanagedMemoryManager<T> : MemoryManager<T> where T : unmanaged
{
    private readonly T* _pointer;
    private readonly int _length;
    private MemoryHandle? _handle;

    public UnmanagedMemoryManager(T* pointer, int length)
    {
        _pointer = pointer;
        _length = length;
    }

    public override Span<T> GetSpan() => new(_pointer, _length);
    public override MemoryHandle Pin(int elementIndex = 0)=> _handle ??= new (_pointer + elementIndex);
    public override void Unpin() => _handle?.Dispose();
    protected override void Dispose(bool disposing) { }
}

三、ReadOnlySequence<T>

ReadOnlySequence<T>代表由一个或者多个连续内存“拼接”而成的只读序列,下图演示了一个典型的”三段式序列。“单段式”序列本质上就是一个ReadOnlyMemory<T>对象,“多段式”序列则由多个ReadOnlyMemory<T>多个借助ReadOnlySequenceSegment<T>连接而成。

image

ReadOnlySequenceSegment<T>是一个抽象类,它表示组成序列的一个片段。ReadOnlySequenceSegment<T>是对一个ReadOnlyMemory<T>对象(对应Memory属性)的封装,同时利用Next属性连接下一个片段,另一个RunningIndex属性表示序列从头到此的元素总量。

public abstract class ReadOnlySequenceSegment<T>
{
    public ReadOnlyMemory<T> Memory { get; protected set; }
    public ReadOnlySequenceSegment<T>? Next { get; protected set; }
    public long RunningIndex { get; protected set; }
}

结构体SequencePosition定义如下,它表示ReadOnlySequence<T>序列的某个“位置”。具体来说,GetObject方法返回的对象代表具有连续内存布局的某个对象,可能是托管数组、非托管指针,还可能是一个字符串对象(如果泛型参数类型为char)。GetInteger返回针对该对象的“偏移量”。

public readonly struct SequencePosition
{
    public object? GetObject();
    public int GetInteger();
    public SequencePosition(object? @object, int integer);
}

ReadOnlySequence<T>结构体的成员定义如下,我们可以通过Length属性得到序列总长度,通过First和FirstSpan属性以ReadOnlyMemory<T>和ReadOnlySpan<T>的形式得到第一个连续的内存片段,通过Start和End属性得到以SequencePosition结构表示起止位置,还可以通过IsSingleSegment确定它是否是一个“单段”序列。通过四个构造函数重载,我们可以利用Array、ReadOnlyMemory<T>和ReadOnlySequenceSegment<T>来创建ReadOnlySequence<T>结构。

public readonly struct ReadOnlySequence<T>
{
public long Length { get; }
public bool IsEmpty { get; }
public bool IsSingleSegment { get; }
public ReadOnlyMemory<T> First { get; }
public ReadOnlySpan<T> FirstSpan { get; }
public SequencePosition Start { get; }
public SequencePosition End { get; }

public ReadOnlySequence(T[] array);
public ReadOnlySequence(T[] array, int start, int length);
public ReadOnlySequence(ReadOnlyMemory<T> memory);
public ReadOnlySequence(ReadOnlySequenceSegment<T> startSegment, int startIndex, ReadOnlySequenceSegment<T> endSegment, int endIndex);

public ReadOnlySequence<T> Slice(long start, long length);
public ReadOnlySequence<T> Slice(long start, SequencePosition end);
public ReadOnlySequence<T> Slice(SequencePosition start, long length);
public ReadOnlySequence<T> Slice(int start, int length);
public ReadOnlySequence<T> Slice(int start, SequencePosition end);
public ReadOnlySequence<T> Slice(SequencePosition start, int length);
public ReadOnlySequence<T> Slice(SequencePosition start, SequencePosition end);
public ReadOnlySequence<T> Slice(SequencePosition start);
public ReadOnlySequence<T> Slice(long start);

public Enumerator GetEnumerator();
public SequencePosition GetPosition(long offset);
public long GetOffset(SequencePosition position);
public SequencePosition GetPosition(long offset, SequencePosition origin);
public bool TryGet(ref SequencePosition position, out ReadOnlyMemory<T> memory, bool advance = true);
}

利用定义的若干Slice方法重载,我们可以对一个ReadOnlySequence<T>对象进行“切片”。GetPosition方法根据指定的偏移量得到所在的位置,而GetOffset则根据指定的位置得到对应的偏移量。TryGet方法根据指定的位置得到所在的ReadOnlyMemory<T> 。我们还可以利用foreach对ReadOnlySequence<T>实施遍历,迭代器通过GetEnumerator方法返回。

四、创建“多段式”ReadOnlySequence<T>

“单段式”ReadOnlySequence<T>本质上就相当于一个ReadOnlyMemory<T>对象,“多段式”ReadOnlySequence则需要利用ReadOnlySequenceSegment<T>将多个ReadOnlyMemory<T>按照指定的顺序“串联”起来。如下这个BufferSegment<T>类型提供了简单的实现。

var segment1 = new BufferSegment<int>([7, 8, 9]);
var segment2 = new BufferSegment<int>([4, 5, 6], segment1);
var segment3 = new BufferSegment<int>([1, 2, 3], segment2);

var index = 1;
foreach (var memory in new ReadOnlySequence<int>(segment3, 0, segment1, 3))
{
    var span = memory.Span;
    for (var i = 0; i < span.Length; i++)
    {
        Debug.Assert(span[i] == index++);
    }
}


public sealed class BufferSegment<T> : ReadOnlySequenceSegment<T>
{
    public BufferSegment(T[] array, BufferSegment<T>? next = null) : this(new ReadOnlyMemory<T>(array), next)
    { }
    public BufferSegment(T[] array, int start, int length, BufferSegment<T>? next = null) : this(new ReadOnlyMemory<T>(array, start, length), next)
    { }
    public BufferSegment(ReadOnlyMemory<T> memory, BufferSegment<T>? next = null)
    {
        Memory = memory;
        Next = next;
        BufferSegment<T>? current = next;
        while (current is not null)
        {
            current.RunningIndex += memory.Length;
            current = current.Next as BufferSegment<T>;
        }
    }
}

五、高效读取ReadOnlySequence<T>

由于ReadOnlySequence<T>具有“单段”和“多段”之分,在读取的时候应该区分这两种情况以实现最高的性能。比如我们在处理缓冲内容的时候,经常会读取前4个字节内容来确定后续内容的长度,就应该按照如下所示的这个TryReadInt32方法来实现。如代码所示,我们先判断ReadOnlySequence<
byte
>的长度大于4个字节,然后再切取前四个字节。如果切片是一个“单段式”ReadOnlySequence<
byte
>(大概率是),我们直接读取FirstSpan属性返回的ReadOnlySpan<byte>就可以了。如果是多段式,为了避免创建一个字节数组,而是采用stackalloc关键字在线程堆栈中创建一个4字节的Span<byte>,并将切片内容拷贝其中,然后读取其中内容即可。由于长度已经读取出来了,我们最后还应该重置ReadOnlySequence<
byte
>将前4个字节剔除。

static bool TryReadInt32(ref ReadOnlySequence<byte> buffer, out int? value)
{
    if (buffer.Length < 4)
    {
        value = null;
        return false;
    }

    var slice = buffer.Slice(buffer.Start, 4);
    if (slice.IsSingleSegment)
    {
        value = BinaryPrimitives.ReadInt32BigEndian(slice.FirstSpan);
    }
    else
    {
        Span<byte> bytes = stackalloc byte[4];
        slice.CopyTo(bytes);
        value = BinaryPrimitives.ReadInt32BigEndian(bytes);
    }

    buffer = buffer.Slice(slice.End);
    return true;
},

其实针对ReadOnlySequence<T>的读取还有更简单的方式,那就是直接使用SequenceReader,比如上面这个TryReadInt32方法也可以写成如下的形式。

static bool TryReadInt32(ref ReadOnlySequence<byte> buffer, out int? value)
{
    var reader = new SequenceReader<byte>(buffer);
    if (reader.TryReadBigEndian(out int v))
    {
        value = v;
        buffer = buffer.Slice(4);
        return true;
    }
    value = null;
    return false;
}