当前位置: 首页 > news >正文

The Go Blog 01:反射的法则(译文)

反思的法则
罗伯-派克
2011 年 9 月 6 日

引言

计算机中的反射是指程序检查自身结构的能力,尤其是通过类型检查自身结构的能力;它是元编程的一种形式。它也是造成混乱的一个重要原因。

在本文中,我们试图通过解释 Go 中的反射是如何工作的来澄清问题。每种语言的反射模型都不尽相同(许多语言根本不支持反射),但本文是关于 Go 的,因此在本文的其余部分,"反射 "一词应理解为 “Go 语言中的反射”。

2022 年 1 月添加的注释:这篇博文写于 2011 年,早于 Go 中的参数多态性(又称泛型)。尽管由于 Go 语言的发展,文章中没有任何重要的内容变得不正确,但为了避免混淆熟悉现代 Go 语言的人,我们还是对一些地方进行了调整。

类型和接口

因为反射建立在类型系统之上,所以我们先来复习一下 Go 中的类型。

Go 是静态类型的。每个变量都有一个静态类型,即在编译时已知并固定的类型:int、float32、*MyType、[]byte 等等。如果我们声明

type MyInt intvar i int
j MyInt

那么 i 的类型是 int,j 的类型是 MyInt。变量 i 和 j 具有不同的静态类型,尽管它们具有相同的底层类型,但如果不进行转换,就不能相互赋值。

接口类型是类型的一个重要类别,它代表固定的方法集。(在讨论反射时,我们可以忽略在多态代码中使用接口定义作为约束)。接口变量可以存储任何具体(非接口)值,只要该值实现了接口的方法。io.Reader和io.Writer是一对著名的例子,它们是io包中的Reader和Writer类型:

// Reader is the interface that wraps the basic Read method.
type Reader interface {Read(p []byte) (n int, err error)
}// Writer is the interface that wraps the basic Write method.
type Writer interface {Write(p []byte) (n int, err error)
}

任何实现了具有此签名的读(或写)方法的类型都被称为实现了 io.Reader(或 io.Writer)。在本讨论中,这意味着io.Reader 类型的变量可以容纳任何具有读取方法的值:

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

需要明确的是,无论 r 的具体值是什么,r 的类型始终是 io.Reader: Go 是静态类型的,r 的静态类型就是 io.Reader。

接口类型的一个极其重要的例子是空接口:

interface{}

或其对应的别名、

any

它表示方法的空集,任何值都可以满足它,因为每个值都有零个或多个方法。

有人说 Go 的接口是动态类型的,但这是一种误导。它们是静态类型的:接口类型的变量总是具有相同的静态类型,即使在运行时存储在接口变量中的值可能会改变类型,该值也总是满足接口的要求。

我们需要准确地理解这一切,因为反射和接口密切相关。

接口的表示

Russ Cox 写过一篇关于 Go 中接口值表示的详细博文。我们没有必要在此重复全部内容,但有必要做一个简化的总结。

接口类型的变量存储一对值:分配给变量的具体值和该值的类型描述符。更准确地说,值是实现接口的底层具体数据项,而类型描述的是该数据项的完整类型。例如,在

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {return nil, err
}
r = tty

从示意代码上看,r 包含 (value, type) 对 (tty、*os.File)。请注意,*os.File 类型实现了 Read 以外的其他方法;尽管接口值只提供了对 Read 方法的访问,但其中的值包含了该值的所有类型信息。这就是我们可以这样做的原因:

w io.Writer
w = r.(io.Writer)

这个赋值中的表达式是一个类型断言;它断言 r 中的项目也实现了 io.Writer,因此我们可以将其赋值给 w。接口的静态类型决定了接口变量可以调用哪些方法,即使里面的具体值可能有更多的方法集。

我们可以继续这样做

var empty interface{}
empty = w

我们的空接口值 empty 将再次包含相同的一对(tty、*os.File)。这很方便:空接口可以容纳任何值,并包含我们所需的关于该值的所有信息。

(这里我们不需要类型断言,因为我们静态地知道 w 满足empty interface{})。在将一个值从 Reader 移到 Writer 的例子中,我们需要明确地使用类型断言,因为 Writer 的方法不是 Reader 方法的子集)。

一个重要的细节是,接口变量内部的变量对总是以 (value, concrete type)的形式存在,而不能以(value, interface type).的形式存在。接口不保存接口值。

现在我们可以进行反射了。

反映的第一定律

1. 反射从接口值到反射对象。

从根本上说,反射只是一种机制,用于检查存储在接口变量中的类型和值对。开始时,我们需要了解包 reflect 中的两种类型: Type and Value。这两种类型可以访问接口变量的内容,而两个简单的函数,即 reflect.TypeOf 和 reflect.ValueOf,可以从接口值中获取 reflect.Type 和 reflect.Value 片段。(此外,从reflect.Value也很容易获取相应的reflect.Type,但我们现在还是把值和类型的概念分开吧)。

让我们从 TypeOf 开始:

package mainimport ("fmt""reflect"
)func main() {var x float64 = 3.4fmt.Println("type:", reflect.TypeOf(x))
}

该程序将打印

type: float64

您可能想知道接口在哪里,因为程序看起来像是将float64变量x传递给reflect.TypeOf,而不是接口值。但它就在那里;正如 godoc 报告的那样,reflect.TypeOf 的签名包含一个空接口:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用 reflect.TypeOf(x) 时,x 首先被存储在一个空接口中,然后将该接口作为参数传递;reflect.TypeOf 会解压该空接口以恢复类型信息。

当然,reflect.ValueOf 函数会恢复值(从这里开始,我们将省略模板,只关注可执行代码):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

打印

value: <float64 Value>

(我们明确调用了 String 方法
(我们明确调用 String 方法,是因为默认情况下 fmt 包会挖掘 reflect.Value 以显示其中的具体值。而 String 方法不会)。

reflect.Type 和 reflect.Value 都有很多方法供我们检查和操作。一个重要的例子是,Value 有一个 Type 方法,用于返回 reflect.Value 的 Type。另一个例子是,Type 和 Value 都有一个 Kind 方法,该方法返回一个常量,表示存储的是什么类型的项目: 如 Uint、Float64、Slice 等。此外,Value 上名称为 Int 和 Float 的方法也能让我们抓取存储在其中的值(如 int64 和 float64):

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

打印

type: float64
kind is float64: true
value: 3.4

还有 SetInt 和 SetFloat 等方法,但要使用这些方法,我们需要了解可设置性,即下文讨论的反射第三定律的主题。

反射库有几个特性值得一提。首先,为了保持应用程序接口的简洁,Value 的 "getter "和 "setter "方法都是在能容纳值的最大类型上操作的:例如,int64 表示所有带符号的整数。也就是说,Value 的 Int 方法返回的是 int64,而 SetInt 值取值的是 int64;可能有必要转换为相关的实际类型:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())   

第二个属性是,反射对象的 Kind 描述的是底层类型,而不是静态类型。如果一个反射对象包含一个用户定义的整数类型的值,如

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v 的 Kind 仍然是 reflect.Int,尽管 x 的静态类型是 MyInt,而不是 int。换句话说,即使类型可以区分 int 和 MyInt,Kind 也不能。

反射第二定律

2. 反射从反射对象到接口值。

与物理反射一样,Go 中的反射也会产生自己的逆反。

给定一个 reflect.Value,我们可以使用 Interface 方法恢复一个接口值;实际上,该方法将 type and value信息打包回接口表示中,并返回结果:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

因此,我们可以说

y := v.Interface().(float64) // y 的类型是 float64。
fmt.Println(y)

来打印反射对象 v 所代表的 float64 值。

不过,我们还可以做得更好。fmt.Println、fmt.Printf 等的参数都以空接口值的形式传递,然后由 fmt 包在内部解包,就像我们在前面的示例中所做的那样。因此,要正确打印 reflect.Value 的内容,只需将 Interface 方法的结果传递给格式化打印例程即可:

fmt.Println(v.Interface())

(自本文撰写以来,对 fmt 软件包进行了修改,使其能像这样自动解压缩 reflect.Value,因此我们可以直接说

fmt.Println(v)

就能得到同样的结果,但为了清晰起见,我们在这里保留 .Interface() 调用)。

由于我们的值是 float64,因此我们甚至可以使用浮点格式:

fmt.Printf("value is %7.1e\n", v.Interface())

并在本例中得到

3.4e+00

同样,我们也不需要将 v.Interface() 的结果类型验证为 float64;空接口值内部包含了具体值的类型信息,Printf 将恢复它。

简而言之,Interface 方法就是 ValueOf 函数的逆过程,只不过它的结果总是静态的 interface{} 类型。

重申: 反射从接口值到反射对象,再返回接口值。

反射的第三定律

3. 要修改反射对象,其值必须是可设置的。

第三定律是最微妙、最容易混淆的,但如果我们从第一条原则出发,还是很容易理解的。

下面是一些不起作用但值得研究的代码。

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果运行这段代码,会出现以下提示信息

panic: reflect.Value.SetFloat using unaddressable value

问题不在于值 7.1 不可寻址,而在于 v 不可设置。可设置性是reflection Value的一个属性,并非所有reflection Value都具有该属性。

在我们的例子中,Value 的 CanSet 方法会报告 Value 的可设置性、

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

打印

settability of v: false

在不可设置的值上调用设置方法是一个错误。但什么是可设置性呢?

可设置性有点像可寻址性,但更严格。它是反射对象可以修改用于创建反射对象的实际存储空间的属性。可设置性取决于反射对象是否持有原始项目。当我们说

var x float64 = 3.4
v := reflect.ValueOf(x)

时,我们向 reflect.ValueOf 传递了 x 的副本,因此作为 reflect.ValueOf 参数创建的接口值是 x 的副本,而不是 x 本身。因此,如果语句

v.SetFloat(7.1)

会更新存储在反射值中的 x 的副本,而 x 本身不会受到影响。这样做既混乱又无用,因此是非法的,而可设置性正是用来避免这一问题的属性。

如果这看起来很奇怪,其实不然。实际上,这是一个我们熟悉的情况,只是披上了不寻常的外衣。想想把 x 传递给函数

f(x)

我们不会指望 f 能够修改 x,因为我们传递的是 x 值的副本,而不是 x 本身。如果我们想让 f 直接修改 x,就必须向函数传递 x 的地址(即指向 x 的指针):

f(&x)

这既简单又熟悉,反射也是如此。如果我们想通过反射修改 x,就必须给反射库一个指向我们要修改的值的指针。

让我们开始吧。首先,我们像往常一样初始化 x,然后创建一个指向它的反射值,称为 p。

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

目前的输出结果是

type of p: *float64
settability of p: false

反射对象 p 不可设置,但我们要设置的不是 p,而是(实际上)*p。为了获取 p 指向的内容,我们调用了 Value 的 Elem 方法,该方法通过指针进行间接操作,并将结果保存在名为 v 的reflection Value 中:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

正如输出结果所示,现在 v 是一个可设置的反射对象、

settability of v: true

由于 v 代表 x,我们终于可以使用 v.SetFloat 来修改 x 的值了:

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

正如预期的那样,输出结果是

7.1
7.1

反射可能很难理解,但它确实在做语言所做的事情,尽管通过反射类型和值可以掩盖正在发生的事情。请记住,反射值需要某些东西的地址,以便修改它们所代表的内容。

Structs

在我们前面的示例中,v 本身并不是一个指针,它只是从指针派生出来的。出现这种情况的常见方法是使用反射来修改结构体的字段。只要我们有结构体的地址,就可以修改它的字段。

下面是一个分析 struct value t 的简单示例。我们用结构体的地址创建反射对象,因为我们稍后要修改它。然后,我们将 typeOfT 设置为其类型,并使用直接的方法调用遍历字段(详见包 reflect)。请注意,我们从结构类型中提取了字段的名称,但字段本身是普通的 reflect.Value 对象。

type T struct {A intB string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {f := s.Field(i)fmt.Printf("%d: %s %s = %v\n", i,typeOfT.Field(i).Name, f.Type(), f.Interface())
}

该程序的输出结果是

0: A int = 23
1: B string = skidoo

这里还顺便介绍了一个关于可设置性的要点:T 的字段名是大写的(导出),因为只有结构体的导出字段才是可设置的。

因为 s 包含一个可设置的反射对象,所以我们可以修改结构体的字段。

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

结果如下

t is now {77 Sunset Strip}

如果我们修改程序,使 s 是根据 t 而不是 &t 创建的,那么对 SetInt 和 SetString 的调用就会失败,因为 t 的字段将不可设置。

Conclusion

这里又是反射法则:
Reflection goes from interface value to reflection object.
Reflection goes from reflection object to interface value.
To modify a reflection object, the value must be settable.

一旦你理解了这些定律,Go中的反射就会变得更容易使用,尽管它仍然很微妙。这是一个强大的工具,除非必要,否则应该小心使用。
还有很多我们没有涉及到的问题——在channel上发送和接收、分配内存、使用slices和map、调用方法和函数——但是这篇文章已经足够长了。我们将在后面的文章中讨论其中的一些主题。

相关文章:

The Go Blog 01:反射的法则(译文)

反思的法则 罗伯-派克 2011 年 9 月 6 日 引言 计算机中的反射是指程序检查自身结构的能力&#xff0c;尤其是通过类型检查自身结构的能力&#xff1b;它是元编程的一种形式。它也是造成混乱的一个重要原因。 在本文中&#xff0c;我们试图通过解释 Go 中的反射是如何工作的…...

Visual Studio Code前端开发插件推荐

引言 Visual Studio Code&#xff08;简称VS Code&#xff09;是一款轻量级且强大的开源代码编辑器&#xff0c;广受前端开发者的喜爱。其丰富的插件生态系统为前端开发提供了许多便利和增强功能的插件。本篇博客将向大家推荐一些在前端开发中常用且优秀的插件&#xff0c;并提…...

jps(JVM Process Status Tool):虚拟机进程状况工具

jps&#xff08;JVM Process Status Tool&#xff09;&#xff1a;虚拟机进程状况工具 列出正在运行的虚拟机进程&#xff0c;并显示虚拟机执行主类名称&#xff08;Main Class&#xff0c;main()函数所在的类&#xff09;以及这些进程的本地虚拟机唯一ID&#xff08;LVMID&am…...

初阶c语言:实战项目三子棋

前言 大家已经和博主学习有一段时间了&#xff0c;今天讲一个有趣的实战项目——三子棋 目录 前言 制作菜单 构建游戏选择框架 实现游戏功能 模块化编程 初始化棋盘 打印棋盘 玩家下棋 电脑下棋 时间戳&#xff1a;推荐一篇 C语言生成随机数的方法_c语言随机数_杯浅…...

计网第三章(数据链路层)(三)

一、点对点协议PPP 在第一篇里有提到数据链路层的信道分为两种&#xff1a;点对点信道和广播信道。 PPP协议就属于点对点信道上的协议。 如果对前面数据链路层的三个基本问题了解的比较透彻&#xff0c;那么这一块很多东西都很好理解。 从考试的角度来讲&#xff0c;PPP协议…...

蓝桥杯每日N题 (砝码称重)

大家好 我是寸铁 希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不…...

Opencv 视频的读取与写入

目录 前言 通过路径获取视频内容 获取视频内容 检查是否正确打开 循环播放 完整代码 从摄像头读取视频数据 获取视频设备 其他与直接读取视频一致 完整实例 录制视频 用于创建视频编解码器的四字符码&#xff08;FourCC&#xff09; cv2.VideoWriter() 将视频帧…...

LeetCode 833. Find And Replace in String【字符串,哈希表,模拟】1460

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…...

Cesium轨迹漫游及视角切换

飞行漫游&#xff0c;就是让Camera飞行。Camera有一些方法可以实现位置、视角的调整&#xff0c;比如flyTo&#xff0c;setView方法。但这些方法并不能沿着我们想要的路径调整&#xff0c;在通过插值的方法不停的调用setView&#xff0c;但这样会造成视图卡顿&#xff0c;而且计…...

构建去中心化微服务集群,满足高可用性和高并发需求的实践指南!

随着互联网技术的不断发展&#xff0c;微服务架构已经成为了开发和部署应用程序的一种主流方式。然而&#xff0c;当应用程序需要满足高可用性和高并发需求时&#xff0c;单一中心化的微服务架构可能无法满足性能和可靠性的要求。因此&#xff0c;构建一个去中心化的微服务集群…...

开集输出和开漏输出

​​​​​​ 首先指明一下以下8中GPIO输入输出模式&#xff1a; GPIO_Mode_AIN 模拟输入&#xff1b; GPIO_Mode_IN_FLOATING 浮空输入&#xff1b; GPIO_Mode_IPD 下拉输入&#xff1b; GPIO_Mode…...

解决内网GitLab 社区版 15.11.13项目拉取失败

问题描述 GitLab 社区版 发布不久&#xff0c;搭建在内网拉取项目报错&#xff0c;可能提示 unable to access https://github.comxxxxxxxxxxx: Failed to connect to xxxxxxxxxxxxxGit clone error - Invalid argument error:14077438:SSL routines:SSL23_GET_S 15.11.13ht…...

【MySQL--->表的约束】

文章目录 [TOC](文章目录) 一、表的约束概念二、空属性约束三、default约束四、zerofill约束五、主键约束六、auto_increment(自增长)约束七、唯一键约束八、外键约束 一、表的约束概念 表通过约束可以保证插入数据的合法性,本质是通过技术手段,保证插入数据收约束,保证数据的…...

github中Keyless Google Maps API在网页中显示地图和标记 无需api key

使用Google Maps API在网页中显示地图和标记的示例博客。以下是一个简单的示例&#xff1a; C:\pythoncode\blog\google-map-markers-gh-pages\google-map-markers-gh-pages\index.html 介绍&#xff1a; 在本篇博客中&#xff0c;我们将学习如何使用Google Maps API在网页中…...

ComPDFKit PDF SDK for Windows Crack

ComPDFKit PDF SDK for Windows Crack 添加了在创建文本框时调整默认属性的支持。 增加了对调整PDF大小时调整宽度的支持。 添加了对编辑文本时更多快捷方式的支持。 优化了文本输入&#xff0c;并将字体样式与原始文本相匹配。 在内容编辑器模式下复制和粘贴时优化了UI交互。 …...

React+Typescript 状态管理

好 本文 我们来说说状态管理 也就是我们的 state 我们直接顺便写一个组件 参考代码如下 import * as React from "react";interface IProps {title: string,age: number }interface IState {count:number }export default class hello extends React.Component<I…...

stable diffusion 运行时报错: returned non-zero exit status 1.

运行sh run.sh安装stable diffusion时报错&#xff1a;ImportError: cannot import name builder from google.protobuf.internal (stable-diffusion-webui/venv/lib/python3.8/site-packages/google/protobuf/internal/__init__.py) 原因&#xff1a;python版本过低&#xff0…...

el-popover弹窗修改三角样式或者位置

el-popover中设置类名 popper-class"filepopver"&#xff0c;我这位置是placement"top-start" <el-popover placement"top-start" popper-class"filepopver" class"filename" width"300" trigger"hover&q…...

Linux驱动开发之点亮三盏小灯

头文件 #ifndef __HEAD_H__ #define __HEAD_H__//LED1和LED3的硬件地址 #define PHY_LED1_MODER 0x50006000 #define PHY_LED1_ODR 0x50006014 #define PHY_LED1_RCC 0x50000A28 //LED2的硬件地址 #define PHY_LED2_MODER 0x50007000 #define PHY_LED2_ODR 0x50007014 #define…...

【SA8295P 源码分析】71 - QAM8295P 原理图参考设计 之 MIPI DSI 接口硬件原理分析

【SA8295P 源码分析】71 - QAM8295P 原理图参考设计 之 MIPI DSI 接口硬件原理分析 一、MIPI-DSI 接口介绍二、高通参考硬件原理图分析:ANX7625 桥接芯片方案2.1 高通参考设计:两路 4-Lane DSI 接口2.2 高通参考设计:DSI0 硬件原理图,将 4 Lane DSI数据通过 ANX7625 桥接芯…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

P3 QT项目----记事本(3.8)

3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

sqlserver 根据指定字符 解析拼接字符串

DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南

精益数据分析&#xff08;97/126&#xff09;&#xff1a;邮件营销与用户参与度的关键指标优化指南 在数字化营销时代&#xff0c;邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天&#xff0c;我们将深入解析邮件打开率、网站可用性、页面参与时…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险

C#入门系列【类的基本概念】&#xff1a;开启编程世界的奇妙冒险 嘿&#xff0c;各位编程小白探险家&#xff01;欢迎来到 C# 的奇幻大陆&#xff01;今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类&#xff01;别害怕&#xff0c;跟着我&#xff0c;保准让你轻松搞…...