2024年9月


张量(Tensor)是PyTorch最基本的操作对象。在几何定义中,张量是基于标量、向量和矩阵概念的眼神。通俗理解,可以讲标量视为0维张量,向量视为1维张量,矩阵视为2维张量。在深度学习领域,可以将张量视为一个数据的水桶,当水桶中只放一滴水时就是0维张量,多滴水排成一排就是1维张量,联排成面就是2维张量,以此类推,扩展到n维向量。

张量的数据类型

PyTorch中创建张量的方法有两种,一种是通过Python数组创建,另一种是从列表中创建。创建张量的代码如下:

import torch
# 从python数组构建
a = [[1, 2, 3],[4, 5, 6]]
x = torch.Tensor(a)
print(a, x)
# 从列表构建张量
x = torch.Tensor([[1, 2]])
print(x)

torch.rand()函数

torch.rand()函数的用法为torch.rand(
\*sizes,out=None
)->Tensor,返回一个张量,服从区间为[0,1)的均匀分布,形状有参数sizes定义,代码如下:

tensor1 = torch.rand(4)
tensor2 = torch.rand(2, 3)
print(tensor1, tensor2)

torch.randn()函数

torch.randn()函数的用法为torch.randn(
*size,out=None
)->Tensor,返回一个张量(高斯白噪声),服从均值为0,方差为1的标准正态分布,形状由参数sizes定义,代码如下:

tensor1 = torch.randn(5)
tensor2 = torch.randn(2, 4)
print(tensor1, tensor2)

torch.normal()函数

torch.normal()函数的第一种用法为torch.normal(means, std, out=None)。

如果没有size参数,返回一个张量,服从指定均值means和标准差std的正态分布,均值means是一个张量,包含每个输出元素相关的正态分布的均值。标准差std是一个张量,包含每个输出元素相关的正态分布的标准差。均值和标准差的形状无需匹配,但每个张量的元素须相同。

tensor = torch.normal(mean=torch.arange(1., 11.), std= torch.arange(1, 0, -0.1))
print(tensor)

torch.arange(1, 0, -0.1)

tensor = torch.normal(mean=0.5, std=torch.arange(1., 6.))
print(tensor)

tensor = torch.normal(mean=torch.arange(1., 6.), std=1.0)
print(tensor)

torch.normal()的第二种用法为torch.normal(mean, std, size*, out=None),共享均值和方程

tensor = torch.normal(2, 3, size=(1, 4))
print(tensor)

该代码中产生的所有的数服从均值为2,方差为3的正态分布,形状为(1,4)

torch.linspace()函数

torch.linspace()函数的用法为torch.linspace(start,end,step=100,out=None)->Tensor,返回一个1维张量,包含在区间start和end上均匀间隔的steps个点。

tensor = torch.linspace(1, 10, steps=5)
print(tensor)

torch.manual_seed()函数

torch.manual_seed()函数用于固定随机种子,下面生成的随机数序列都是相同的,仅作用于最接近的随机数产生语句。

torch.manual_seed(1)
temp1 = torch.rand(5)
print(temp1) 
torch.manual_seed(1)
temp2 = torch.rand(5)
print(temp2)  
temp3 = torch.rand(5)
print(temp3)  

temp1和temp2是相同的,temp3是不同的,因为seed仅作用于最接近的那句随机数产生语句。torch中toech.seed()函数没有参数,用来将随机数的种子设置为随机数,一般不使用。

torch.ones()、torch.zeros()、torch.eye()

torch.ones()函数用来生成全1数组,torch.zeros()函数用来生成全0数组,torch.eye()函数用来生产对角线为1,其余部分全为0的二维数组。

tensor1 = torch.zeros(2, 3)
tensor2 = torch.ones(2, 3)
tensor3 = torch.eye(3)
print(tensor1, tensor2, tensor3)

在PyTorch中默认的数据类型为32位浮点型(torch.FloatTensor),如果想要规定张量的数据类型,可以在创建时指定类型,或者创建完之后对张量的数据类型进行转换。

# 第一种方法:在创建张量时指定数据类型
x = torch.ones((2, 3, 4), dtype=torch.int64)  # 生成全1数组
print(x)
# 第二种方法:张量创建完成后,对数据类型进行转换
x = torch.ones(2, 3, 4)  # 生成全1数组
x = x.type(torch.int64)
print(x)

张量的基本操作

PyTorch中,张量的操作分为结构操作和数学运算,结构操作就是改变张量本身的结构,数学操作就是对张量的元素值进行数学运算。常用的结构操作包括改变张量的形状、增加和删除维度、交换维度、拼接和分隔、堆叠和分解、索引和切片,数学运算包括基本数学运算、向量运算、矩阵运算。

torch.view()函数可以改Tensor的形状,但是必须保证前后元素总数一致。

x = torch.rand(3, 2)
print(x.shape)  # torch.Size([3, 2])
y = x.view(2, 3)
print(y.shape)  # torch.Size([6])

增加和删除维度

unsqueeze()函数可以对数据维度进行扩充,给指定位置加上维度为1的维度。用法:torch.unsqueeze(a,N),或者a.unsqueeze(N),在a中指定位置N加上一个维度为1的维度。

# 增加维度
a = torch.rand(3, 4)
b = torch.unsqueeze(a, 0)
c = a.unsqueeze(0)
print(b.shape) 
print(c.shape)  

squeeze()函数可以对张量进行维度的压缩,去掉维数为1的维度。用法:torch.squeeze(a)将a中所有维数为1的维度都删除,或者a.squeeze(1)是去掉a中指定维数为1的维度。

# 删除维度
a = torch.rand(1, 1, 3, 4)
b = torch.squeeze(a)
c = a.squeeze(1)
print(b.shape)  
print(c.shape)  

交换维度

在运用各种模型的过程中,经常会遇到交换维度的问题。PyTorch中有两种交换维度的方法。torch.transpose()函数用于交换两个维度,torch.permute()函数可以自由交换任意位置。

a = torch.rand(1, 3, 28, 32)  # torch.Size([1, 3, 28, 32]
# 第一种方法
b = a.transpose(1, 3).transpose(1, 2)  # torch.Size([1, 28, 32, 3])
print(b.shape)

# 第二种方法
c = a.permute(0, 2, 3, 1)
print(c.shape)  # torch.Size([1, 28, 32, 3])

拼接和分割

可以用torch.cat()函数将多个张量合并,torch.cat()函数是链接,不会增加维度;也可以用torch.split()函数把一个张量分割为多个张量。torch.split()函数可以看做torch.cat()函数的逆运算。split()函数的作用是将张量拆分为多个块,每个块都是原始张量的视图。

# torch.cat()拼接方法的代码如下:
a = torch.rand(1, 2)
b = torch.rand(1, 2)
c = torch.rand(1, 2)
output1 = torch.cat([a, b, c], dim=0)  # dim=0为按列拼接
print(output1.shape)  # torch.Size([3, 2])
output2 = torch.cat([a, b, c], dim=1)  # dim=1为按行拼接
print(output2.shape)  # torch.Size([1, 6])

# torch.split()分割方法的代码如下:
a = torch.rand(3, 4)
output1 = torch.split(a, 2)
print(output1)
output2 = torch.split(a, [2, 2], dim=1)
print(output2)

堆叠和分解

可以用torch.stack()函数将多个张量合并,torch.stack()函数和torch.cat()函数有略微的差别,torch.stack()函数用于进行堆叠操作,会增加一个维度。torch.chunk()函数可以看做torch.cat()函数的逆运算。torch.chunk()函数的作用是将Tensor按dim(行或列)分割成chunks个Tensor块,返回的是一个元组。

# torch.stack()堆叠方法
a = torch.rand(1, 2)
b = torch.rand(1, 2)
c = torch.rand(1, 2)
output1 = torch.stack([a, b, c], dim=0)  # dim=0为按列拼接
print(output1.shape)  
output2 = torch.stack([a, b, c], dim=1)  # dim=1为按行拼接
print(output2.shape)  

# torch.chunk()分解方法的代码如下:
a = torch.rand(3, 4)
output1 = torch.chunk(a, 2, dim=0)
print(output1)
output2 = torch.chunk(a, 2, dim=1)
print(output2)

索引和切片

Torch张量的索引以及切片规则与Numpy基本一致,比较简单。

x = torch.rand(2, 3, 4)
print(x[1].shape)  
y = x[1, 0:2, :]
print(y.shape)  
z = x[:, 0, ::2]
print(z.shape) 

基本数学运算

张量基本的数学运算包括张量求和、张量元素乘积、对张量求均值、方差和极值。

元素求和

按元素求和的第一种方法为torch.sum(input)->float,返回输入向量input中所有的元素的和。

# 元素求和第一种方法
a = torch.rand(4, 3)
b = torch.sum(a)
print(b)  # tensor(6.4069)

按元素求和的第二种方法为torch.sum(input,dim,keepdim=False,out=None)->Tensor,其中input为输入张量,dim为指定维度,keep(bool)表示输出张量是否保持与输入张量有相同数量的维度,ruokeepdim值为True,则在输出张量中,出了被操作的dim维度值降为1,其他维度与输入张量input相同。否则,dim维度相当于被执行torch.squeeze()维度压缩,导致此维度消失,最终输出张量会比输入张量少一个维度。

# 元素求和第二种方法
a = torch.rand(4, 3)
b = torch.sum(a, dim=1, keepdim=False)
print(b, b.shape)

按索引求和

按索引求和是指按索引参数index中所确定的顺序,将参数张量Tensor中的元素与执行本方法的张量的元素逐个相加。参数Tensor的尺寸必须严格执行方法的张量匹配,否则会发生错误。按索引求和的方法为torch.Tensor.index_add_(dim,index,tensor)->Tensor,dim为索引index所指向的维度,index为包含索引数的张量,Tensor为含有相加元素的张量。

# 按索引求和,不常用
x = torch.Tensor([[1, 2],[3, 4]])
y = torch.Tensor([[3, 4],[5, 6]])
index = torch.LongTensor([0, 1])
output = x.index_add_(0, index, y)
print(output)

元素乘积

元素乘积的第一种方法为torch.prod(input)->float,返回输入张量input所有元素的乘积。

# 元素乘积第一种方法
a = torch.rand(4, 3)
b = torch.prod(a)
print(b) 

元素乘积的第二种方法为torch.prod(input,dim,keepdim=False,out=None)->Tensor,其中input为输入张量,dim为指定维度,keepdim(bool)表示输出张量是否保持与输入张量有相同数量的维度,若keepdim值为True,则在输出张量中,除了被操作的dim维度值降为1,其他维度与输入张量input相同。否则,dim维度相当于被执行torch.squeeze()函数进行维度压缩操作,导致此维度消失,最终输出张量回避输入张量少一个维度。

# 元素乘积第二种方法
a = torch.rand(4, 3)
b = torch.prod(a, 1, True)
print(b, b.shape)

求平均数

求平均数的第一种方法为torch.mean(input),返回输入张量input中每个元素的平均值。

# 求平均数的第一种方法
a = torch.rand(4, 3)
b = torch.mean(a)
print(b) 

求平均数的第二种方法为torch.mean(input,dim,keepdim=False,out=None),其中参数的含义与元素求和、元素乘积的含义相同。

# 求平均数的第二种方法
a = torch.rand(4, 3)
b = torch.mean(a, 1, True)
print(b, b.shape)

求方差

求方差的第一种方法为torch.var(input,unbiased=True)->float,返回输入向量input中所有元素的方差,unbiased(bool)表示是否使用基于修正贝塞尔函数的无偏估计。

# 求方差的第一种方法
a = torch.rand(4, 3)
b = torch.var(a)
print(b)

求方差的第二种方法为torch.var(input,dim,keepdim=False,unbiased=True,out=None)->Tensor,unbiased(bool)表示是否使用基于修正贝塞尔函数的无偏估计,其余参数含义与元素求和、元素乘积含义相同。

# 求方差的第二种方法
a = torch.rand(4, 3)
b = torch.var(a, 1, True)
print(b, b.shape)

求最大值

求最大值的第一种方法为torch.max(input)->float,返回输入张量所有元素的最大值。

# 求最大值的第一种方法
a = torch.rand(4, 3)
b = torch.max(a)
print(b)

求最大值的第二种方法为torch(input,dim,keepdim=False,out=None)->(Tensor,LongTensor),返回新的张量input中指定维度dim中每行的最大值,同时返回每个最大值的位置索引。

# 求最大值的第二种方法
a = torch.rand(4, 3)
b = torch.max(a, 1, True)
print(b)

求最小值

求最小值的第一种方法为torch.min(input)->float,返回输入张量的最小值。

# 求最小值的第一种方法
a = torch.rand(4,3)
b = torch.min(a)
print(b) 

求最小值的第二种方法为torch.min(input,dim,keepdim=False,out=None)->(Tensor,LongTensor),与求最大值的第二种方法类似。

# 求最小值的第二种方法
a = torch.rand(4, 3)
b = torch.min(a, 1, True)
print(b)

向量运算和矩阵运算

向量的线性代数运算包括点乘(dot(a,b))、内积(inner(a,b))、叉乘(matmul(a,b))等。

向量的点乘

向量的待你橙又称为向量的内积或数量积,对两个向量执行点乘运算,就是对这两个向量对应位一一相乘之后求和的操作。

# 向量的点乘,a和b必须为一维
a = torch.Tensor([1, 2, 3])
b = torch.Tensor([1, 1, 1])
output = torch.dot(a, b)
print(output) # 等价于 1*1+2*1+3*1,tensor(6.)

向量的叉乘

两个向量的外积,又叫叉乘、叉积向量积,其运算结果是一个向量而不是一个标量。

# 向量的叉乘
a = torch.Tensor([1, 2, 3])
b = torch.Tensor([1, 1, 1])
output = torch.multiply(a, b)
print(output)

矩阵的内积

两个维度相同的矩阵a和b,a和b矩阵的内积是相同位置的向量的内积。

# 矩阵的内积
a = torch.Tensor([1, 2, 3])
b = torch.Tensor([1, 1, 1])
output = torch.inner(a, b)
print(output) 

矩阵的外积

矩阵的外积就是通常意义的一般矩阵乘积。一般矩阵乘积是矩阵相乘最重要的方法,它只有在第一个矩阵的列数(column)和第二个矩阵的行数(row)相同时才有意义。

# 矩阵的外积:矩阵乘法
a = torch.Tensor([[1, 2, 3], [4, 5, 6]])
b = torch.Tensor([[1, 1], [2, 2], [3, 3]])
output = torch.matmul(a, b)
print(output)

# 按批量相乘
a = torch.randn(10, 3, 4)
b = torch.randn(10, 4, 5)
output = torch.bmm(a, b)
print(output.shape)
# tensor([[14., 14.],
#         [32., 32.]])

torch.einsum():爱因斯坦求和约定是一个非常神奇的函数,号称可以满足你的一切需求!功能强大,

https://zhuanlan.zhihu.com/p/44954540

张量与Numpy数组

由于使用Numpy中ndarray处理数据非常方便,经常会将张量与Numpy数组进行相互转换,所以掌握两者之间的转换方法很有必要。但是需要注意一点:相互转换后所产生的张量和Numpy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变。将张量转换为Numpy数组使用tensor.numpy()方法,将Numpy数组转换为张量使用torch.from_numpy(ndarry)方法。

张量转Numpy数组

使用numpy()函数将张量转换成Numpy数组。

a = torch.ones(1, 2)
b = a.numpy()  # 进行转换
print(a, b)  
a += 2
print(a, b)  
b += 2  # 在a改变后,b也已经改变
print(a, b) 

Numpy数组转张量

使用from_numpy()函数将Numpy数组转换成张量。

import numpy as np
a = np.ones([1, 2])
b = torch.from_numpy(a)  # 进行转换
print(a, b)  
a += 2
print(a, b)  
b += 2  # 在a改变后,b也已经改变
print(a, b)  

Cuda张量与CPU张量

在深度学习过程中,GPU能起到加速的作用。PyTorch中的张量默认存放在CPU设备中,如果GPU可用,可以将张量转移到GPU中。CPU张量转换为Cuda张量有两种方法。一般情况下,可以使用.cuda方法将Tensor移动到GPU;在有多个GPU的情况下,可以使用to方法来确定使用哪个设备。也可以使用.cpu方法将Tensor移动到CPU。

x = torch.rand(2, 4)
print(x.device)  # cpu
# 第一种方法
x = x.cuda()
print(x.device)  # cuda:0
# 第二种方法
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if torch.cuda.is_available():
    x = x.to(device)
    print(x.device)  #cuda:0
# 转化为cpu
x = x.cpu()
print(x.device)  # cpu

欢迎关注公众号:愚生浅末。
image

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


Go plan9 汇编系列文章:


0. 前言

在 Go plan9 汇编系列文章中,介绍了函数和函数栈的调用。这里继续看内存对齐和递归调用方面的内容。

1. 内存对齐

直接上示例:

type temp struct {
	a bool
	b int16
	c []string
}

func main() {
	var t = temp{a: true, b: 1, c: []string{}}
	fmt.Println(unsafe.Sizeof(t))
}

输出:

32

改写 temp 结构体成员变量位置:

type temp struct {
	a bool
    c []string
	b int16
}

func main() {
	var t = temp{a: true, b: 1, c: []string{}}
	fmt.Println(unsafe.Sizeof(t))
}

输出:

40

为什么移动下结构体成员的位置会对结构体在内存中的大小有影响呢?

打印示例中结构体成员变量地址如下:

# 示例 1
func main() {
	var t = temp{a: true, b: 1, c: []string{}}

	fmt.Println(unsafe.Sizeof(t.a), unsafe.Sizeof(t.b), unsafe.Sizeof(t.c))
	fmt.Printf("%p %p %p %p\n", &t, &t.a, &t.b, &t.c)

	fmt.Println(unsafe.Sizeof(t))
}

# go run ex10.go 
1 2 24
0xc0000a4000 0xc0000a4000 0xc0000a4002 0xc0000a4008
32

# 示例 2
func main() {
	var t = temp{a: true, b: 1, c: []string{}}

	fmt.Println(unsafe.Sizeof(t.a), unsafe.Sizeof(t.c), unsafe.Sizeof(t.b))
	fmt.Printf("%p %p %p %p\n", &t, &t.a, &t.c, &t.b)

	fmt.Println(unsafe.Sizeof(t))
}

# go run ex10.go 
1 24 2
0xc00006e090 0xc00006e090 0xc00006e098 0xc00006e0b0
40

可以看到,在为结构体分配内存时是要遵循内存对齐的,内存对齐是为了简化寻址,CPU 可一次找到变量的位置。因为内存对齐的存在,这里示例 2 中虽然变量 a 只占 1 个字节,但却独占了 8 个字节,这对于写代码来说是一种内存消耗,应当避免的。

2. 递归

我们看一个递归的示例:

func main() {
	println(sum(1000))
}

//go:nosplit
func sum(n int) int {
	if n > 0 {
		return n + sum(n-1)
	} else {
		return 0
	}
}

输出:

# go run ex7.go 
# command-line-arguments
main.sum: nosplit stack over 792 byte limit
main.sum<1>
    grows 24 bytes, calls main.sum<1>
    infinite cycle

这里我们在
sum
函数前加
//go:nosplit
是要声明这个函数是不可栈分裂的函数。意味着当函数栈满的时候,(内存分配器)不会为它开辟新的空间。

Go 为 goroutine 分配的初始栈空间大小为 2K,如果 main 栈加上 nosplit 的 sum 栈超过 2K,将导致爆栈。


//go:nosplit
拿掉,重新执行:

func main() {
	println(sum(100000))
}

func sum(n int) int {
	if n > 0 {
		return n + sum(n-1)
	} else {
		return 0
	}
}

输出:

5000050000

那么
sum
是否可以无限递归呢?我们给
sum
一个大数 10000000000000,接着重新执行:

runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc0200f8398 stack=[0xc0200f8000, 0xc0400f8000]
fatal error: stack overflow

输出
stack overflow
,main 协程的栈是从 0xc0200f8000 到 0xc0400f8000,这里递归所用的栈超过了 goroutine 栈的最大限制
1000000000-byte
(超过的意思是 main 栈加上 sum 递归调用的栈超过了最大限制),也就是 1G。


前言

大家好,推荐一个.NET 8.0 为核心,结合前端 Vue 框架,实现了前后端完全分离的设计理念。它不仅提供了强大的基础功能支持,如权限管理、代码生成器等,还通过采用主流技术和最佳实践,显著降低了开发难度,加快了项目交付速度。

如果你需要一个高效的开发解决方案,本框架能帮助大家轻松应对挑战,实现业务目标。

项目介绍

YuebonCore 是一款基于 .NET 8.0 的现代化权限管理及快速开发框架,专为前后端分离设计。它集成了组织机构管理、角色用户管理、权限分配、多系统和多应用支持、定时任务调度、业务单据编码规则以及代码生成器等核心模块,为开发者提供了一站式的开发体验。

该框架充分利用了最新的技术栈,包括 ASP.NET Core MVC、SqlSugar ORM、WebAPI、Swagger 文档自动生成工具以及 Vue.js 前端框架,确保了开发过程中的高效与便捷。

YuebonCore 架构简洁、易用性强,非常适合中小规模项目的快速启动与迭代。

功能特点

  • 开源许可:
    采用 MIT 许可协议,完全开源。
  • 主流框架:
    基于流行技术栈,易于学习和使用。

  • 二次开发:
    支持灵活的定制化开发,满足大多数项目需求。

  • 代码生成器:
    减少 70% 的重复编码工作,提高开发效率。

  • 精细权限控制:
    导航菜单与功能按钮级权限管理。

  • 数据权限管理:
    精确控制数据访问范围,如部门、组或公司级别。

  • 常用类封装:
    日志、缓存、验证等功能模块。

适用范围

适用范围广泛,可用于开发包括 OA、ERP、BPM、CRM、WMS、TMS、MIS、BI、电商平台后台、物流管理系统、快递管理系统以及教务管理系统在内的各类管理软件。

内置功能

1、系统设置:对系统动态配置常用参数。

2、用户管理:用户是系统操作者,该功能主要完成系统用户配置。

3、组织机构:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。

4、角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。

5、字典管理:对系统中经常使用的一些较为固定的数据进行维护。

6、功能模块:配置系统菜单,操作权限,按钮权限标识等。

7、定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。

8、代码生成:前后端代码的生成(.cs、.vue、.js)代码。

9、日志管理:系统正常操作日志、登录日志记录和查询;系统异常信息日志记录和查询。

10、多应用管理:支持应用调用 api 授权控制。

11、多系统管理:实现各子系统的统一管理和授权。

13、业务单据编码规则:可以按常量、日期、计数、时间等自定义业务单据编码规则。

14、短信和邮件:集成腾讯云短信通知和 EMail 发送通知

15、支持租户模式

16、支持用户定义主题风格

17、支持一主多从数据库读写分离

项目技术

前端目前采用 Vue 独立前端和 ASP.NET Core MVC 模式,使用的技术栈有些区别,后期将侧重于 Vue 端的优化运维。

1、前端技术

基于 Vue3、Vite、Vuex、Vue-router 、Vue-cli 、axios 和 element-plus

前端采用 VS Code 工具开发

2、传送门

element-plus 官网

https://element-plus.org/#/zh-CN

vue-element-admin

https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/

3、后端技术

  • 核心框架:.NET 8.0 +SqlSugar + AutoMapper + WebAPI + swagger
  • 定时计划任务:Quartz.Net 组件
  • 安全支持:过滤器、Sql 注入、请求伪造
  • 服务端验证:实体模型验证、自己封装 Validator
  • 缓存框架:微软自带 Cache、Redis
  • 日志管理:Log4net、登录日志、操作日志
  • 工具类:NPOI、MiniProfiler 性能分析、验证码、丰富公共功能

项目结构

Yuebon.NetCore 解决方案包含

  • Yuebon.Commons[基础类库]
    :包框架的核心组件,包含一系列快速开发中经常用到的 Utility 辅助工具功能,框架各个组件的核心接口定义,部分核心功能的实现;

  • Yuebon.Security.Core[权限管理类库]
    :以 Security 为基础实现以角色-功能、用户-功能的功能权限实现,以角色-数据,用户-数据的数据权限的封装

  • Yuebon.AspNetCore[AspNetCore 类库]
    ,提供 AspNetCore 的服务端功能的封装,支持 webapi 和 webmvc 模式,同时支持插件式开发;

  • Yuebon.WebApp[管理后台]:基于 aspnet core mvc 实现了权限管理和 CMS 部分管理后台。

  • Yuebon.Cms.Core[CMS 基础类库],包含文章管理、广告管理等内容,以此做案例给大家开发参考

  • Yuebon.WebApi[webapi 接口]
    :为 Vue 版或其他三方系统提供接口服务。

DataBase 是最新数据库备份文件,目前支持 MS SQL Server 和 MySql。

项目效果

1、管理平台体验地址:http://default.ts.yuebon.com 有租户管理模块

2、测试租户体验地址:http://tenant1.ts.yuebon.com 无租户管理模块

体验账号密码均为:admin/admin888

登录页面

系统模块和功能管理

角色管理

代码生成器
支持一键生成服务端代码和前端代码,复制粘贴简单快速高效实现功能

项目地址

Gitee:
https://gitee.com/yuebon/YuebonNetCore

WebAPI:
http://netcoreapi.ts.yuebon.com

官方文档:
http://docs.v.yuebon.com/

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!

六,Spring Boot 容器中 Lombok 插件的详细使用,简化配置,提高开发效率

@


1. Lombok 介绍

Lombok 作用:

  1. 简化 Java Bean开发,可以使用 Lombok 的注解让代码更加简洁。
  2. Java项目中,很多没有技术含量但又必须存在的代码;比如:Pojo 的 getter/setter/toString ;异常处理:I/O流的关闭操作等等。
  3. Java项目中,很多没有技术含量但又必须存在的代码:比如:这些代码既没有技术含量,又影响着代码的美观,Lombok 应运而生。

Spring Boot 和 IDEA 官方支持

  1. IDEA 2020已经内置了 Lombok插件
  2. Spring Boot 2.1.x之后的版本也在 Stater中内置了 Lombok 依赖

2. Lombok 常用注解

@Data:注解在类上,提供类所有属性的getting和setting方法,此外还提供了equals,canEqual,hashCode,toString方法
@Setter: 注解在属性上,为属性提供 setting 方法,注解在类上,则为类中的所有属性提供 set()方法。
@Getter: 注解在属性上,为属性提供 getting 方法注解在类上,则为类中的所有属性提供 get()方法。
@Log4j:注解在类上:为类提供了一个属性名为: log 的 log4j的日志对象
@NoArgsConstructor: 注解在类上,为类提供一个无参的构造方法,(一定会提供)
@AllArgsConstructor: 注解在类上,为类提供一个全参的构造方法
@Cleanup: 可以关闭流
@Builder: 被注解的类加个构造者模式
@Synchronized: 加同步锁 
@SneakyThrows: 等同于try/catcher捕获异常
@NonNull:如果给参数加个这个注解,参数为null会抛出空指针异常
@Value: 注解和@Value类似,区别在于它会把所有成员变量默认定义为 private final 修饰,并且不会生产set()方法 

下面我们测试,使用几个,在开发中比较常用的
注解

首先想要使用上 Lombok 需要导入相关
jar
依赖,也可以不用特别导入,直接使用 Spring Boot 自带内置的即可。

在这里插入图片描述

   <!--        引入 lombok ,使用版本仲裁-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_lombok</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>


    <dependencies>
        <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
        <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--        引入 lombok ,使用版本仲裁-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

2.1 @ToString

@ToString: // 在编译时,生成 toString,注意:默认情况下,会生成一个无参构造器。

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;




@ToString  // 在编译时,生成 toString,注意:默认情况下,会生成一个无参构造器
public class Furn {
    private Integer id = 100;
    private String name = "张三";
    private Double price = 999.0;


}

使用 lombok 注解简化代码,可以通过 idea 自带的反编译功能 target,看Furn.class的源码,就
可以看到生成的完整代码。

如果目录当中没有 target 目录显示,可以进行如下操作:

在这里插入图片描述

在这里插入图片描述

2.2 @Setter

@Setter 注解: 注解在属性上,为属性提供 setting 方法,注解在类上,则为类中的所有属性提供 set()方法。

首先,我们先注解使用在属性上。

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;


public class Furn {

    @Setter // 注解在属性上,编译生成一个,该属性的 set()方法:默认情况下,会生成一个无参构造器
    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

添加到类上,为类中所有的属性都添加,set() 方法。

在这里插入图片描述

在这里插入图片描述

2.3 @Data

@Data :注解在类上,提供类所有属性的getting和setting方法,此外还提供 equals,canEqual,hashCode,toString方法, @RequiredArgsConstructor

特别说明: @Data 中的 @RequiredArgsConstructor
在我们写controller 或是 Service层的时候,需要注入很多的 mapper接口或者另外的service接口,这时候就会写很多
的@Autowired注解,代码看起来很乱。Lombok 提供了一个注解:
@RequiredArgsConstructor(onConstructor=@_(@AutoWired))
写在类上可以代替@Autowired注解,需要注意的时在注入时,需要用final定义,或者使用 @notnull注解

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;



@Data// 注解等价使用了,如下注解: @Getter,@Setter,@RequiredArgsConstructor @ToString,@EqualsAndHas
public class Furn {

    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

2.4 @AllArgsConstructor

@AllArgsConstructor:在编译时,会生成全参数构造器

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;



@AllArgsConstructor // 在编译时,会生成全参数构造器
public class Furn {

    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

注意:这里:我们会发现一点,就是有全参数构造器,但是,却没有生成一个默认的“无参数构造器”

2.5 @NoArgsConstructor

@NoArgsConstructor: 在编译时,会生成无参构造器(一定会生成),不会受到其它的

在这里插入图片描述

package com.rainbowsea.springboot.bean;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;


@NoArgsConstructor //在编译时,会生成无参构造器(一定会生成),不会受到其它的
public class Furn {

    private Integer id = 100;

    private String name = "张三";
    private Double price = 999.0;


}

在这里插入图片描述

特别说明:

特别说明,虽然上面的@Data,@ToString注解等等,默认情况下都会生成一个无参构造器,但是当你使用了多个注解的时候,可能会覆盖掉无参构造器。 但是当我们有其它多个构造器生成时,你如果你希望仍然有无参构造器就需要使用 @NoArgsConstructor 注解了,因为 @NoArgsConstructor是一定会生成一个无参构造器的(无参构造器很重要,因为框架的使用是涉及到反射机制的,而反射机制,需要一个无参构造器,否则你就无法进行反射获取 bean对象,框架也就无法使用了)。

如下:测试。我们添加上 @Data注解和 @AllArgsConstructor 注解。可以明显的发现,
默认的无参数构造器被覆盖掉了

在这里插入图片描述

所以我们需要添加上:@NoArgsConstructor注解,因为@NoArgsConstructor注解,在编译时,会生成无参构造器(一定会生成),不会受到其它的影响,被覆盖。如图

在这里插入图片描述

3. 在 idea 中 安装 lombok插件

不装插件也可以用基本的注解比如:@Data,@Getter...
但是不能使用其扩展功能,比如日志输出...,所以我们还是安装一下,也比较简单。
直接去插件商城,搜索: Lombok 即可

在这里插入图片描述

在这里插入图片描述

package com.rainbowsea.springboot.controller;


import com.rainbowsea.springboot.bean.Furn;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@Slf4j
@RestController  // @Controller + @ResponseBody
public class HiController {


    @Autowired
    private Furn furn;


    @RequestMapping("/hi") // 设置请求映射路径
    public String hi(){
        log.info("furn-"+furn);

        // 占位符方式输出
        log.info("furn={} myfurn={}",furn,furn);


        return  "hi word";
        // 使用 slf4j日志输出
    }
}

运行测试:

在这里插入图片描述

4. 总结:

  1. 熟悉常用的 Lombok 注解,提供开发效率。
  2. 基本上@注解都会生成一个默认的无参构造器,但是当存在多个注解的时候,这个默认的无参构造器,可能会被覆盖掉,从而导致无法不会被生成。但是,框架的上的使用,基本上都是需要使用上
    反射
    机制的,而反射机制是
    必须要有一个无参构造器
    才可以进行反射,获取到对应的 Bean对象。给框架使用。所以,一般的 bean 对象都会添加上一个@NoArgsConstructor注解该在编译时,会生成无参构造器(一定会生成),不会受到其它的

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

带箭头的直线
就是有方向的直线,既可以用来表示
矢量
,也可以用来
标记
某个关键位置。
manim
中提供了
4种
常用的
带箭头的直线
模块:

  1. Arrow
    :单箭头的直线
  2. DoubleArrow
    :双箭头的直线
  3. LabeledArrow
    :带标签的直线
  4. Vector
    :向量

其中,
DoubleArrow

LabeledArrow

Vector
都继承自
Arrow
模块,

Arrow
模块继承自上一次介绍的
Line
模块。

Vector
表面看起来
Arrow
一样,也是一个单箭头直线,它们的主要区别在于:
Arrow
需要设置
起点

终点
,而
Vector
只要设置
终点
,它的的
起点
固定为
坐标系原点

image.png
带箭头直线系列
的模块位于
manim
中的
Mobject
之下。

1. 主要参数

Arrow
模块的主要参数有:

参数名称 类型 说明
start Point3D 起点
end Point3D 终点
stroke_width float 箭头的粗细
buff float 箭头的长短
max_tip_length_to_length_ratio float 箭头大小与线的粗细的比例,此值越大,箭头相对于线越大
max_stroke_width_to_length_ratio float 线的粗细与箭头大小的比例,此值越大,线相对于箭头线越粗
tip_shape VMobject 箭头的类型,本质就是一个几何对象

其中,
start

end
参数继承自
Line
模块。

DoubleArrow
模块的参数与
Arrow
基本一致,只是它两头都有箭头。

Vector
模块的主要参数有:

参数名称 类型 说明
direction Point2D/Point3D 向量的方向,相当于终点

Vector
模块只需要设置终点,也就是
direction
。其他参数可以参考
Arrow
模块。

模块
LabeledArrow
可以在箭头直线上加一个便签,用于添加额外的说明信息。

参数名称 类型 说明
label str 标签中文字,可支持数学公式
label_position float 标签位置,默认在直线正中间
font_size float 标签字体大小
label_color Color 标签颜色
label_frame bool 是否显示标签背景
frame_fill_color Color 标签背景颜色
frame_fill_opacity float 标签背景透明度

2. 主要方法

Arrow
模块的主要方法有:

名称 说明
get_default_tip_length 获取箭头的长度
get_normal_vector 获取向量的法线
reset_normal_vector 重置向量的法线
scale 缩放向量

DoubleArrow

LabeledArrow
的方法和
Arrow
一样。

Vector
除了上面
Arrow
的方法,还有一个自己特有的方法:

名称 说明
coordinate_label 基于向量的坐标显示向量的值

比如:

class ArrowExample(Scene):
    def construct(self):
        vec_1 = Vector([1, 3])
        vec_2 = Vector([-2, -2])
        label_1 = vec_1.coordinate_label(color=BLUE)
        label_2 = vec_2.coordinate_label(color=YELLOW)

        self.add(vec_1, vec_2, label_1, label_2)

vector.png

3. 使用示例

3.1. 箭头和线的比例

通过
buff

max_stroke_width_to_length_ratio

max_tip_length_to_length_ratio
参数,
可以调整箭头和直线的比例,使之符合不同动画场景的需要。

# buff
vg = VGroup()
for buff in np.arange(0, 2, 0.5):
    vg.add(
        Arrow(
            buff=buff,
            start=2 * LEFT,
            end=2 * RIGHT,
        )
    )

vg.arrange(DOWN)
vg.move_to(2.5 * LEFT)
self.play(Create(vg), run_time=run_time)

# max_stroke_width_to_length_ratio
vg = VGroup()
for i in np.arange(0, 5, 1):
    vg.add(
        Arrow(
            max_stroke_width_to_length_ratio=i,
        ),
    )

vg.arrange(DOWN)
self.play(Create(vg), run_time=run_time)

# max_tip_length_to_length_ratio
vg = VGroup()
for i in np.arange(0, 0.3, 0.06):
    vg.add(
        Arrow(
            max_tip_length_to_length_ratio=i,
        ),
    )

out.gif

3.2. 箭头的样式

箭头的样式不仅仅只有三角形,
manim
中内置了多种不同的箭头样式。

Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowCircleFilledTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowCircleTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowSquareFilledTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowSquareTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowTriangleFilledTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=ArrowTriangleTip,
)
Arrow(
    start=2 * LEFT,
    end=2 * RIGHT,
    tip_shape=StealthTip,
)

out.gif

3.3. 向量的样式

向量
是一种特殊的
带箭头直线
,它的起点默认在坐标原点。
其他的属性也可以和上面带箭头的一样进行设置。

vec1 = Vector([1, 1], color=BLUE)

vec2 = Vector(
    [-2, 1],
    color=RED,
    tip_shape=ArrowSquareTip,
)
label2 = vec2.coordinate_label(color=RED)

vec3 = Vector([2, -1.5], color=GREEN)

out.gif

3.4. 带标签的箭头

最后,
带箭头
的直线和普通的直线一样,也可以添加标签信息,用于对直线进行说明。

LabeledArrow(
    label="y=kx+b",
    font_size=25,
    start=start,
    end=end,
)
LabeledArrow(
    label=txt1,
    start=start,
    end=end,
)
LabeledArrow(
    label="z=\sqrt{x^2+y^2}",
    font_size=25,
    start=start,
    end=end,
    label_color=RED,
    label_frame=False,
)
LabeledArrow(
    label=txt2,
    start=start,
    end=end,
    frame_fill_color=GREEN,
    frame_fill_opacity=0.8,
)

out.gif

4. 附件

文中完整的代码放在网盘中了(
arrow.py
),
下载地址:
完整代码
(访问密码: 6872)