2024年8月

《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,全书共分10章,第1章主要让读者认识数据资产,了解数据资产相关的基础概念,以及数据资产的发展情况。第2~8章主要介绍大数据时代数据资产管理所涉及的核心技术,内容包括元数据的采集与存储、数据血缘、数据质量、数据监控与告警、数据服务、数据权限与安全、数据资产管理架构等。第9~10章主要从实战的角度介绍数据资产管理技术的应用实践,包括如何对元数据进行管理以发挥出数据资产的更大潜力,以及如何对数据进行建模以挖掘出数据中更大的价值。

图书介绍:
数据资产管理核心技术与应用

今天主要是给大家分享一下第五章的内容:

第五章的标题为数据服务

内容思维导图如下:

在数据资产中,数据服务是对外提供使用和访问的一种最重要的形式,数据只有提供对外的访问,才能体现其自身的价值。而且有了数据服务后也可以降低外部业务或者用户使用数据的门槛,对于用户来说提供了如下的便利。

  • 不需要用户自己花时间从数据资产中去检索数据。
  • 不要用户自己考虑怎么来获取自己已经检索到的数据。
  • 不需要考虑获取数据时的网络安全等问题。
  • 数据服务可以复用于多个用户和业务。
  • 用户不需要关注技术细节,可以专注于数据服务的具体需求。

另外数据质量以及数据的监控与告警,会直接决定数据服务的质量,因为如果数据质量低下以及数据在存在问题时不能及时得到监控与告警,这样数据服务被用户访问时,就无法获取到准确的数据。

《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

1、如何设计数据服务

在传统的API接口服务中,通常会面临如下的一些问题:

  • 一个普通的接口服务通常需要数据ETL开发工程师按照数据产品经理提供的业务逻辑进行数据处理加工得到最终的结果数据,然后再由后端开发工程师开发接口服务,并且输出接口服务的文档,之后再让业务通过接口来获取最终的结果数据。由于经过的环节很多,导致时间周期一般需要很长,如下图所示。

  • 当业务众多时,对外输出的数据接口服务势必也会很多,随着人员的交替、数据需求逻辑的变更等,数据服务的管理会非常困难,需要维护的接口数量越来越多,运维成本非常大。
  • 通常在时间久了后,由于业务方以及数据服务开发和运维人员的交替,会遇到每个数据服务有哪些业务在调用、每个业务的调用量有多少、数据服务在变更和升级时,需要通知到哪些业务等众多繁琐的问题。

所以数据服务的设计非常的重要,在设计时,通常需要考虑如下问题:《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

  • 数据服务的敏捷化和可配置化:敏捷化是指数据服务可以随着业务需求的变化,能快速的进行修改和变更。可配置化是指数据服务不需要使用太多的代码开发,而是根据配置SQL查询逻辑、配置请求的入参和响应报文,就能快速生成一个对外使用的数据服务。
  • 数据服务API文档可自动生成:能根据数据服务的请求入参和响应报文,自动生成API文档,不需要后端开发工程师每次手动进行编写,而且手动编写文档还极易出错,并且每次变更时都需要及时更新和维护。
  • 数据服务的流程化管理:流程化管理是指可以在一个平台上完成数据服务调用的申请、审批、开发、上线、监控、告警等环节,方便数据服务的运维与管理。
  • 数据服务的统一鉴权设计:所有的数据服务采用相同的统一鉴权方式,方便以后的运维和管理。
  • 数据服务的性能以及熔断:性能是数据服务最重要的一项指标,因为如果性能不达标的话,那就无法满足业务的查询要求。熔断是指当数据服务的调用量过大或者发生了大量异常时,为了保护数据服务的整体稳定性,而触发的一种降级机制。
  • 数据服务的监控与告警:数据服务在发生问题时,能通过监控与告警及时让运维或者开发人员进行处理,降低数据服务的问题给业务带来的影响。数据服务如果自身没有监控与告警,在发生问题时,可能就需要通过业务使用时主动来报出服务不可用的问题。

为了解决上述的问题,在大数据资产中,一般建议对数据服务进行平台化设计。开发一个数据服务API 通常的流程如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

上述的这些流程,在做数据服务的平台化设计时,都可以考虑进去,这样通过平台化的管理,就可以达到数据服务的敏捷化开发以及开发时可以基于平台快速配置来生成外部访问需要的数据服务节省人力的成本开销以及缩短开发的周期。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

在平台化的统一管理下,自然就可以在创建数据服务或者变更数据服务时,自动来生成API文档。也可以对数据服务进行统一的鉴权以及监控与告警。

1.1、数据源管理

数据源是数据服务的基础,没有数据源,自然也就无法提供数据服务。数据源的来源可以是多种多样的。数据仓库、数据湖、常见的各种类型的关系型数据库等都可以是数据服务的数据源。而且创建好的一个数据源也可以让多个数据服务共同使用,如下图所示。

在数据服务的平台化建设中,数据源管理的关键就在于需要去适配多种不同的数据源,而每一种数据源通常都有自己专有的数据驱动。所以数据服务平台在底层技术实现时需要能支持动态的去根据不同的数据源加载不同的数据驱动来达到创建数据源连接的目的,如下图所示。

1.2、   数据服务的敏捷化和可配置化

数据服务的敏捷化和可配置化主要体现在能随着业务需求的变化而快速的开发、修改对应的数据服务。而要达到快速响应需求的提出或者变更,将数据服务设计成能配置SQL查询逻辑或者低代码的逻辑脚本、能配置请求的入参和响应报文便是一种最好的解决方式,如下图所示为建议的数据服务可配置的技术实现方案设计。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

从图中可以看到

  • 在创建数据服务时,填入的请求参数,需要和SQL查询脚本中的查询条件对应上。
  • 创建的数据服务在保存时,需要将参数和SQL查询脚本都保存到数据库表中,在设计时,会设计两张表,一张表用于存储数据服务的相关信息,一张表用于存储数据服务对应的数据参数信息。创建数据服务时,数据服务平台会自动生成一个唯一的API服务ID,两张表之间通过API服务ID进行关联。

数据服务在创建好了后,外部用户在调用数据服务时,数据服务平台处理请求的逻辑如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

1.3、 数据服务文档的自动生成

数据服务在支持了可配置化之后,还需要能自动生成数据服务的API文档,这样外部用户在调用数据服务的API时,就知道如何去调用了。由于数据服务的请求参数、服务ID等信息都已经保存在数据服务平台中了,所以通过从数据服务平台中把这些数据信息查询出来,按照API文档的格式展示出来就可以了,如下图所示。

从图中可以看到,由于数据服务平台中保存了数据服务每次修改变更的信息,所以在自动生成出的数据服务文档中,还可以将该数据服务的历史变更记录也展示出来,可以看到自动生成出来的数据服务文档和通常手动编写的那种接口服务文档的内容几乎是一致的。

1.4、数据服务的统一认证与鉴权

数据服务在创建好了后,在调用时必定需要认证,不然就无法保证服务的安全性,也无法防止网络机器人或者黑客等发起的频繁恶意的请求或者攻击等。传统的认证通常是通过密码来完成的,给每个请求方分配一个唯一的密码,然后请求时,每次都带上这个密码,如果密码正确,那么就可以完成请求的调用,如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

但是这种简单的认证方式会面临如下问题:

  • 安全性太低,密码容易丢失或者被别人使用。
  • 通常情况下,很多数据服务都是支持Http协议,但是Http请求非常容易被劫持,一旦被劫持后,就可以从拦截到的请求中获取到密码,这样别人就可以用这个密码来发送请求或者直接篡改劫持到的请求,造成恶意的攻击。

为了解决上述的问题,通常推荐使用数字签名的方式来完成数据服务的统一认证,如下图所示,数字签名通常采用非对称的密码的方式(也称为公私钥机制),请求方通过私钥对请求的报文进行加密。数据服务平台通过使用该私钥对应的公钥进行解密。

从图中可以看到请求方需要先对数据生成摘要,然后再用私钥加密后,生成加密的摘要,然后将数据和加密的摘要一起发送给数据服务平台,数据服务平台在收到数据和加密后的摘要后,先对加密后的摘要使用对应的公钥进行解密得到解密后的摘要,并且对发送过来的数据也重新生成摘要,如果生成出来的摘要和解密后的摘要一致,说明数据在传输过程中没用被更改。数据签名的认证方式可以有效的防止请求被劫持从而发生篡改。

在服务认证时,还可以加入网络认证,这样可以有效的拦截非正常调用的请求,最常见的网络认证的方式就是在网络中设置白名单,如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

从图中可以看到,数据服务首先就会判断请求方的IP地址是否在白名单内,如果不在就可以直接返回。

在服务认证通过后,就可以进行下一步的鉴权操作了,数据服务平台在设计时,通常会给每个请求方分配唯一的身份ID(通常也称作appId),数据服务平台在收到该唯一身份ID后,会判断其是否有调用该数据服务的权限,如下图所示,如果没用权限,那么就直接返回并且给出对应的提示信息。

未完待续......《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

锁作为并发编程中的关键一环,是应该要深入掌握的。

1. 锁

1.1 示例

实现锁很简单,示例如下:

var global int

func main() {
	var mu sync.Mutex
	var wg sync.WaitGroup	

	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			mu.Lock()
			global++
			mu.Unlock()
		}(i)
	}

	wg.Wait()
	fmt.Println(global)
}

输出:

2

在 goroutine 中给全局变量
global
加锁,实现并发顺序增加变量。其中,
sync.Mutex.Lock()
对变量/临界区加锁,
sync.Mutex.Unlock()
对变量/临界区解锁。

1.2 sync.Mutex

我们看
sync.Mutex
互斥锁结构:

type Mutex struct {
	state int32
	sema  uint32
}

其中,
state
表示锁的状态,
sema
表示信号量。

进入
sync.Mutex.Lock()
查看加锁的方法。

1.2.1 sync.Mutex.Lock()

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

首先进入
Fast path
逻辑,原子
CAS
操作比较锁状态
m.state
和 0,如果相等则更新当前锁为已加锁状态。这里锁标志位如下:

image

从低(右)到高(左)的三位表示锁状态/唤醒状态/饥饿状态:

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
)

标志位初始值为 0,1 表示状态生效。

前三位之后的位数表示排队等待锁的 goroutine 数目,总共可以允许 1<<(32-3) 个 goroutine 等待锁。

这里假设有两个 goroutine G1 和 G2 抢占锁,其中 G1 通过
Fast path
获取锁,将锁的状态置为 1。这时候 G2 未获得锁,进入
Slow path

func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false
	iter := 0
	old := m.state
	for {
		// step1: 进入自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}

        ...
    }
}

Slow path
的代码量不大,但涉及状态转换很复杂,不容易看懂。这里拆成每个步骤,根据不同场景分析具体源码。

进入
Mutex.lockSlow()
,初始化各个状态位,将当前锁状态赋给变量
old
,进入 for 循环,执行第一步自旋逻辑。自旋会独占 CPU,让 CPU 空跑,但是减少了频繁切换 goroutine 带来的内存/时间消耗。如果使用的适当,会节省 CPU 开销,使用的不适当,会造成 CPU 浪费。这里进入自旋是很严苛的,通过三个条件判断能否自旋:

  1. 当前锁是普通模式才能进入自旋。
  2. runtime.sync_runtime_canSpin
    需要返回 true:
    • 当前 goroutine 进入自旋的次数小于 4 次;
    • goroutine 运行在多 CPU 的机器上;
    • 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;

假设 G2 可以进入自旋,运行
runtime_doSpin()

# src/runtime/lock_futex.go
const active_spin_cnt = 30

# src/runtime/proc.go
//go:linkname sync_runtime_doSpin sync.runtime_doSpin
//go:nosplit
func sync_runtime_doSpin() {
	procyield(active_spin_cnt)
}

# src/runtime/asm_amd64.s
TEXT runtime·procyield(SB),NOSPLIT,$0-0
	MOVL	cycles+0(FP), AX
again:
	PAUSE
	SUBL	$1, AX
	JNZ	again
	RET

自旋实际上是 CPU 执行了 30 次 PAUSE 指令。

自旋是在等待,等待锁释放的过程。假设在自旋期间 G1 已释放锁,更新
m.state
为 0。那么,在 G2 自旋逻辑中
old = m.state
将更新 old 为 0。继续往下看,for 循环中做了什么。

func (m *Mutex) lockSlow() {
	...
	for {
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			...
		}

        // step2: 更新 new,这里 new 为 0
        new := old

		// step2: 继续更新 new
        // -      如果锁为普通锁,更新锁状态为已锁。如果锁为饥饿锁,跳过饥饿锁更新。
        // -      这里更新锁为 1
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}

        // step2:继续更新 new
        // -      如果锁为已锁或饥饿的任何一种,则更新 new 的 goroutine 排队等待位
        // -      这里锁为已释放,new 为 1
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}

        // step2: 继续更新 new
        // -      如果 goroutine 处于饥饿状态,并且当前锁是已锁的,更新 new 为饥饿状态
        // -      这里锁为已释放,new 为 1
        if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}

        // step2: 继续更新 new
        // -      如果当前 goroutine 是唤醒的,重置唤醒位为 0
        // -      goroutine 不是唤醒的,new 为 1
        if awoke {
			// The goroutine has been woken from sleep,
			// so we need to reset the flag in either case.
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			new &^= mutexWoken
		}

        // step3: CAS 比较 m.state 和 old,如果一致则更新 m.state 到 new
        // -      这里 m.state = 0,old = 0,new = 1
        // -      更新 m.state 为 new,当前 goroutine 获得锁
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 如果更新锁之前的状态不是饥饿或已锁,表示当前 goroutine 已获得锁,跳出循环。
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
            ...
        }
    }
}

这里将自旋后的逻辑简化为两步,更新锁的期望状态 new 和通过原子 CAS 操作更新锁。这里的场景不难,我们可以简化上述流程为如下示意图:

image

2. 小结

本文介绍了 Go 互斥锁的基本结构,并且给出一个抢占互斥锁的基本场景,通过场景从源码角度分析互斥锁。


先看一下效果吧:

原博主的地址:
【动画进阶】神奇的卡片 Hover 效果与 Blur 的特性探究 - ChokCoco - 博客园 (cnblogs.com)

原效果是一个css效果,我们采用WPF的方式模仿一下

因为技术有限,没有原博主的那么好看,毕竟盗版永远比不过原版...

然后这里看一下盗版的怎么写吧

先是拿到原版的图片(原博客里面有图片地址),当然也可以自己挑一张自己喜欢的图片,把图片保存到自己的WPF项目下面

再把图片运用到代码里面去,给一个border的背景设置成图片,再给border设置一个圆角,就会得到一个圆角的图片

    <Grid>
        <Grid x:Name="gd1" Height="400" Width="300">
            <Border Margin="50" CornerRadius="30">
                <Border.Background>
                    <ImageBrush ImageSource="08.jpg"/>
                </Border.Background>
            </Border>
        </Grid>
    </Grid>

然后再给图片添加一个模糊效果

模糊效果的实现就是在图片的下面一层添加一个同样的border,但是设置不同的margin,让它比正常显示的图片大一圈

<Grid x:Name="gd1" Height="400" Width="300">
<Border CornerRadius="30" Margin="18">
<Border.Effect>
<BlurEffect Radius="20"/>
</Border.Effect>
<Border.Background>
<ImageBrush ImageSource="08.jpg"/>
</Border.Background>
</Border>
<Border Margin="50" CornerRadius="30"> <Border.Background> <ImageBrush ImageSource="08.jpg"/> </Border.Background> </Border> </Grid>

红色部分就是添加的模糊层,添加以后就会得到下面的效果

然后最重要的就是剩下的hover效果了:

我们先准备一个背景色,原版的背景色我不知道怎么实现,所以只能用不同的背景色做为底色了

把这个背景放到最下面就会得到下面的效果(
这里遇到个问题,不知道怎么把超出border以外的虚化效果裁掉,导致border以外也有一点点的模糊效果
)

(
原博是通过设置外层的
overflow: hidden; 来实现的,但是我不知道wpf怎么实现这个效果,查了半天也没查到

)

代码如下,就是就是一个带有渐变色的border

<Grid x:Name="gd1" Height="400" Width="300">
    <Border x:Name="bd2" BorderThickness="2" Visibility="Hidden" CornerRadius="30" Margin="15">
<Border.BorderBrush>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF43D4F9"/>
<GradientStop Color="#FFDF07FD" Offset="1"/>
</LinearGradientBrush>
</Border.BorderBrush>
<Border.Clip>
<EllipseGeometry x:Name="eg1" RadiusX="150" RadiusY="150" Center="150,200"/>
</Border.Clip>
<Border CornerRadius="30" Opacity="0.7">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF43D4F9"/>
<GradientStop Color="#FFDF07FD" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
</Border>
<Border CornerRadius="30" Margin="18"> <Border.Effect> <BlurEffect Radius="20"/> </Border.Effect> <Border.Background> <ImageBrush ImageSource="08.jpg"/> </Border.Background> </Border> <Border Margin="50" CornerRadius="30"> <Border.Background> <ImageBrush ImageSource="08.jpg"/> </Border.Background> </Border> </Grid>

然后就是实现hover效果,在实现hover效果以前先给bd2添加一个clip效果

UIElement.Clip 属性 (System.Windows) | Microsoft Learn

关于clip的解释,可以看一下微软对于clip效果的说明

<Border.Clip>
    <EllipseGeometry x:Name="eg1" RadiusX="150" RadiusY="150" Center="150,200"/>
</Border.Clip>

把这段代码放到明为bd2的border下面就可以了

但是还需要一些后台代码,控制一下这个clip的移动,我们给最外层的名为gd1的grid添加几个事件

publicMainWindow()
{
InitializeComponent();
gd1.MouseMove
+=MainGrid_MouseMove;
gd1.MouseLeave
+=Bd1_MouseLeave;
gd1.MouseEnter
+=Bd1_MouseEnter;
}
private void Bd1_MouseEnter(objectsender, MouseEventArgs e)
{
bd2.Visibility
=Visibility.Visible;
}
private void Bd1_MouseLeave(objectsender, MouseEventArgs e)
{
bd2.Visibility
=Visibility.Hidden;
}
private void MainGrid_MouseMove(objectsender, MouseEventArgs e)
{
//这里获取一下鼠标的坐标,然后让clip效果的中心跟着鼠标中心移动 Point mousePosition =e.GetPosition(gd1);
eg1.Center
= newPoint(mousePosition.X, mousePosition.Y);
}

然后我们让默认情况下bd2的 Visibility="Hidden"

效果就完成了...

如果有更好的解决办法,可以和我联系哦。

项目github地址:
bearhanQ/WPFFramework: Share some experience (github.com)

QQ技术交流群:332035933;

欢迎进群讨论问题,不论是winform,还是wpf,还是.net core的,还有很多萌妹.

Snippet Craft

一个VS Code代码片段管理插件

功能

创建和插入代码片段

在编辑器区域右键菜单中点击插入Snippet,或在代码片段视图中点击条目,则会将代码片段插入到当前激活文档的光标位置。

在这里插入图片描述

代码片段编辑

代码片段在左侧栏中,根据创建时的文件内容类型,分组显示代码片段,可编辑已有的代码片段。

在这里插入图片描述

代码片段预览

鼠标移动到代码片段条目上时,显示tooltip预览

在这里插入图片描述

默认映射

映射是插入代码片段时,自动替换的变量。

代码片段中通过设置占位符
${VAR_NAME}
,在插入代码片段时,将自动替换为全局变量中的值。可用的映射如下表:

类别 变量 描述
文件和编辑器相关 TM_SELECTED_TEXT 当前选定的文本或空字符串
TM_CURRENT_LINE 当前行的内容
TM_CURRENT_WORD 光标下的单词或空字符串的内容
TM_LINE_INDEX 基于零索引的行号
TM_LINE_NUMBER 基于一个索引的行号
TM_FILENAME 当前文档的文件名
TM_FILENAME_BASE 当前文档的文件名(不含扩展名)
TM_DIRECTORY 当前文档的目录
TM_FILEPATH 当前文档的完整文件路径
RELATIVE_FILEPATH 当前文档的相对文件路径(相对于打开的工作区或文件夹)
CLIPBOARD 剪贴板的内容
WORKSPACE_NAME 打开的工作区或文件夹的名称
WORKSPACE_FOLDER 打开的工作区或文件夹的路径
CURSOR_INDEX 基于零索引的游标编号
CURSOR_NUMBER 基于单索引的游标编号
时间相关 CURRENT_YEAR 本年度
CURRENT_YEAR_SHORT 当年的最后两位数字
CURRENT_MONTH 两位数字的月份(例如“02”)
CURRENT_MONTH_NAME 月份的全名(例如“July”)
CURRENT_MONTH_NAME_SHORT 月份的简短名称(例如“Jul”)
CURRENT_DATE 以两位数字表示的月份中的某一天(例如“08”)
CURRENT_DAY_NAME 日期的名称(例如“星期一”)
CURRENT_DAY_NAME_SHORT 当天的简短名称(例如“Mon”)
CURRENT_HOUR24 小时制格式的当前小时
CURRENT_MINUTE 两位数的当前分钟数
CURRENT_SECOND 当前秒数为两位数
CURRENT_SECONDS_UNIX 自 Unix 纪元以来的秒数
CURRENT_TIMEZONE_OFFSET 当前 UTC 时区偏移量为 +HH
或者 -HH
(例如“-07:00”)
其他 RANDOM6 6 个随机 Base-10 数字
RANDOM_HEX6 6 个随机 Base-16 数字
UUID 第四版UUID

注意:当自定义映射值未设置或者不可用时,将直接显示变量占位符

自定义映射

扩展初始化时,插入了三个常用的自定义映射,你可以自由更改或添加自定义映射。

在这里插入图片描述

示例:

代码片段内容

value of 'AUTHOR' is: ${AUTHOR}
value of 'COMPANY' is: ${COMPANY}
value of 'MAIL' is: ${MAIL}
value of 'FOOBAR' (non-exist) is: ${FOOBAR}

插入代码片段后,显示如下:

value of 'AUTHOR' is: 林晓lx
value of 'COMPANY' is: my-company
value of 'MAIL' is: jevonsflash@qq.com
value of 'FOOBAR' (non-exist) is: ${FOOBAR}

与自定义映射一样,当默认映射值未设置或者不可用时,将直接显示变量占位符

自动完成

代码片段可在编辑器中显示自动提示和补全内容。

在这里插入图片描述

快速开始

通过点击上方横幅上的安装按钮安装 Snippet Craft,或在 VS Code 的扩展侧边栏中搜索 Snippet Craft 进行安装。

更新内容

Date Version Content
V0.1.0 2024-8-22 初始版本

Todo




作者信息

作者:林小

邮箱:jevonsflash@qq.com

License

The MIT License (MIT)

项目地址

Github:snippet-craft

参数在方法种具有按“值(by value)”和“引用(by ref)”两种传递方式,这是每个.NET程序员深入骨髓得基本概念。但是我若告诉你,.NET规定的参数传递形式其实是三种,会不会颠覆你的认知。

一、官方描述
二、TypedReference结构体
三、三个特殊的方法
四、三种参数传递方式

一、官方描述

三种参数传递方式并非我们杜撰出来的,而是写在.NET最核心的规范文档ECMA-355中(I.12.4.1.5),原文如下:

The CLI supports three kinds of parameter passing, all indicated in metadata as part of the
signature of the method. Each parameter to a method has its own passing convention (e.g., the
first parameter can be passed by-value while all others are passed byref). Parameters shall be
passed in one of the following ways (see detailed descriptions below):

  • By-value – where the value of an object is passed from the caller to the callee.
  • By-reference – where the address of the data is passed from the caller to the callee, and the type of the parameter is therefore a managed or unmanaged pointer.
  • Typed reference – where a runtime representation of the data type is passed along with the address of the data, and the type of the parameter is therefore one specially supplied for this purpose.

It is the responsibility of the CIL generator to follow these conventions. Verification checks that
the types of parameters match the types of values passed, but is otherwise unaware of the details
of the calling convention.

三种参数传递方式如下:

  • By-value:传递参数的值或者拷贝。这里所谓的值分两种情况,对于值类型,变量的值就是承载目标值的字节,比如参数类型是一个我们自定义的结构体,那么传递的是承载这个结构体内容的所有字节;对于引用类型,变量的值是目标对象的内存地址,所以传递的这个地址(4/8字节)的拷贝;
  • By-Reference: 传递的是变量所在的位置(Location),可能是变量在堆栈上的内存地址,或者数组元素在堆上的内存地址。所以方法不仅仅可以从这个地址读取原始参数当前的值,还可以通过填充字节到此位置改变原始的值。对于值类型,被调用方法可以将原始的值“就地”变成一个新的值;对于引用类型,方法则会原来的引用指向一个新的对象。
  • Typed reference:可以认为强类型的引用,在By-Reference基础上还传递参数的类型;

二、TypedReference

基于Typed reference的传递时通过如果这个TypedReference结构体实现的,从其定义可以看出它通过字段_value保持值得引用,并利用_type确定其类型。它定义了一系列静态方法完成一些基于TypedReference得基本操作,比如创建一个TypedReference对象,将一个TypedReference对象转换成Object,获取TypedReference对象得目标类型等;

public struct TypedReference
{
    private readonly ref byte _value;

    private readonly IntPtr _type;

    public unsafe static object ToObject(TypedReference value);
    public unsafe static TypedReference MakeTypedReference(object target, FieldInfo[] flds);
    public static Type GetTargetType(TypedReference value);
    public static RuntimeTypeHandle TargetTypeToken(TypedReference value);
    public static void SetTypedReference(TypedReference target, object value);
}

三、三个特殊的方法

TypedReference还涉及三个如下三个特殊方法或者函数,可能很多开源人员都没有见过:

  • __makeref:创建一个新的TypedReference对象;
  • __reftype:获取引用的目标类型;
  • __refvalue:获取和设置引用的值;

四、三种参数传递方式

我们通过如下这个简单的例子来演示上述的三种参数传递方式,它们分别体现在三个对应的方法上。模拟按照Typed reference进行参数传递的PassByTypedReference方法将参数类型定义为TypedReference,它通过断言检验传递参数的类型(通过调用__reftype方法获取),并通过调用__refvalue修改参数的值。

PassByValue(value);
Debug.Assert(value == int.MinValue);

PassByReference(ref value);
Debug.Assert(value == int.MaxValue);

value = int.MinValue;
PassByTypedReference(__makeref(value));
Debug.Assert(value == int.MaxValue);

static void PassByValue(int v) => v = int.MaxValue;
static void PassByReference(ref int v) => v = int.MaxValue;
static void PassByTypedReference(TypedReference v)
{
    Debug.Assert(__reftype(v) == typeof(int));
    __refvalue(v, int) = int.MaxValue;
}