2024年3月

写在前面

按照国际惯例,要先聊下生活,吐槽一番,今天是2月14日,也是下午听老妈说,我才知道!
现在真的是对日期节日已经毫无概念可言,只知道星期几。

现在已经觉得写博客也好,学习文章也罢,和写日记一样,已经融入到我的生活中,或者更确切的说,变成生活的一部分了。

饭后和老妈闲聊了几句后,我发现现在真的是对做什么都提不起兴趣, 也没有什么特别期待的事,对相亲特别排斥,而且毫无期待而言,甚至没法相信任何一个女孩。

听妈妈说,她和一个阿姨今天在散步时,阿姨告诉妈妈,说我和前任分手后,前任的妈妈到处说我的坏话。

妈妈却很平静的和那个阿姨说,嘴巴长在她身上,我们也管不着,她愿意讲究就讲吧!

我听完刚开始还挺气的,后来,我却很开心,真的庆幸我没和她结婚,就前任的妈妈,也是绝了,都分手了,就各自安好得了呗,为嘛还要不依不饶呢!

布局优化修改

加完复文本编辑器在编辑和新增的model上显示真的好丑,所以打算改个布局吧。

1、使用24 栅格调整布局

将表单和table表格布局做下微调,示例代码如下:

<a-row>
    <a-col :span="12">col-12</a-col>
    <a-col :span="12">col-12</a-col>
</a-row>

2、table表格字段显示优化

去掉父文档和顺序字段显示,示例代码如下:

// 使用reactive进行数据绑定
const columns = [
  {
    title: '名称',
    dataIndex: 'name',
  },
  {
    title: 'Action',
    dataIndex: 'action',
  }
];

3、树形数据默认全部显示

示例代码如下:

<a-table
        v-if="level1 && level1.length"
         :defaultExpandAllRows="true"
         :columns="columns"
         :data-source="level1"
         :pagination="false"
         :loading="loading"
         size="small"
>

4、效果

写在最后

本来以为布局啥的全整完了,结果落到博客上才发现,富文本编辑器没了,直接把一个功能给干掉了,尴尬!

在编写1-4步的过程中,我还处于emo状态,这回我又好了。

哈哈,有点神叨叨的,只看技术部分,碎碎念略,怕影响正在阅读的您,新年快乐,好运连连!

痞子衡嵌入式半月刊: 第 93 期

这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻,农历年分二十四节气,希望在每个交节之日准时发布一期。

本期刊是开源项目(GitHub:
JayHeng/pzh-mcu-bi-weekly
),欢迎提交 issue,投稿或推荐你知道的嵌入式那些事儿。

上期回顾

《痞子衡嵌入式半月刊: 第 92 期》

唠两句

历史上的今天:1990年3月3日,人类第一次胜利徒步横穿南极。

本期共收录 5 个项目,希望对你有帮助!

项目类

1、FP-AUD-SMARTMIC1 - ST开源的各类音频算法

FP-AUD-SMARTMIC1 提供了一个运行在 STM32 上的固件,它采集四个数字 MEMS 麦克风的音频信号,通过嵌入式 DSP 库对其进行细化(包含了波束形成,AEC(回声消除),声源定位,分贝转换等),并将处理后的音频流式传输到连接到相关扩展板的 USB 主机和扬声器上。还提供了通信基础设施,允许从主机 PC 控制设备状态和设置运行算法。

2、raspberry-pi-os - 学习使用Linux内核和树莓派开发操作系统

此项目包含一个循序渐进的指南,教您如何从头开始创建一个简单的操作系统(OS)内核。作者称这个操作系统为树莓派操作系统或RPi操作系统。RPi操作系统的源代码主要基于Linux内核,但是该操作系统的功能非常有限,并且只支持Raspberry PI 3。

3、MiniLisp - 1K行C语言写的Lisp解释器

有一天,项目作者想看看用1000行C代码能做些什么,于是决定写一个Lisp解释器,这变成了一个有趣的周末项目,结果是一个迷你lisp实现。所有这些都在1000行c中,作者没有为了大小而牺牲可读性。在作者看来,代码中有大量注释,以帮助读者理解所有这些特性是如何工作的。

MiniLisp 实现了整数、符号、局部变量、条件语句、宏和垃圾回收等功能。

4、Zstandard - 快速无损的数据压缩算法Zstandard的实现

Zstandard,简称 zstd,是一种快速无损压缩算法,针对 zlib 级的实时压缩场景和更好的压缩比。在相似的压缩比情况下,它解压缩的速度比其他的算法都要快。很多知名项目都有这个算法的身影。

如下表是使用 lzbench (@inikep使用gcc 9.3.0编译的开源内存基准)在 Silesia 压缩语料上测试和比较了几种快速压缩算法:

5、linq4c - C#的linq方法之C实现

C# LINQ(Language Integrated Query)是一种强大且灵活的查询语言,可以将数据查询、过滤、排序和转换等操作无缝集成到 C# 代码中。如果你想在 C 中使用 C# 的 linq 方法,那么不妨来使用这个项目!这是它的 C 语言版。实现了 linq 的大部分方法(60+)。

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

三、设计 LCS-LENGTH的带备忘的版本,运行时间为O(mn)。需要写代码的时候,请用go语言。

文心一言,代码正常运行:

LCS-LENGTH(Longest Common Subsequence Length)问题的带备忘的版本通常指的是使用动态规划(Dynamic Programming, DP)和备忘录(Memoization)来优化算法性能,避免重复计算。通过维护一个表(即“备忘录”)来存储已经计算过的子问题的解,从而在解决新问题时可以直接查找已存储的结果,而不是重新计算。

以下是使用Go语言实现的一个带备忘的LCS-LENGTH算法的示例。在这个示例中,lcsLength 函数计算两个字符串的最长公共子序列长度,并且使用一个二维数组 memo 来存储子问题的解,其中 memo[i][j] 存储的是 str1 的前 i 个字符和 str2 的前 j 个字符的最长公共子序列长度。

package main  
  
import (  
	"fmt"  
)  
  
func lcsLength(str1, str2 string) int {  
	m, n := len(str1), len(str2)  
	memo := make([][]int, m+1)  
	for i := range memo {  
		memo[i] = make([]int, n+1)  
	}  
  
	for i := 1; i <= m; i++ {  
		for j := 1; j <= n; j++ {  
			if str1[i-1] == str2[j-1] {  
				memo[i][j] = memo[i-1][j-1] + 1  
			} else {  
				memo[i][j] = max(memo[i-1][j], memo[i][j-1])  
			}  
		}  
	}  
  
	return memo[m][n]  
}  
  
func max(a, b int) int {  
	if a > b {  
		return a  
	}  
	return b  
}  
  
func main() {  
	str1 := "ABCDEFG"  
	str2 := "XYZABCDEFGHIJKL"  
	length := lcsLength(str1, str2)  
	fmt.Printf("The length of the Longest Common Subsequence is: %d\n", length)  
}

在这个实现中,lcsLength 函数首先初始化一个 (m+1) x (n+1) 的二维数组 memo,其中 m 和 n 分别是两个输入字符串 str1 和 str2 的长度。然后,它使用两个嵌套的循环来遍历 str1 和 str2 的所有字符组合。

如果 str1 的第 i 个字符和 str2 的第 j 个字符相同,则最长公共子序列的长度加一,即 memo[i][j] = memo[i-1][j-1] + 1。如果不同,则最长公共子序列的长度是去掉 str1 的第 i 个字符后的最长公共子序列和去掉 str2 的第 j 个字符后的最长公共子序列中的较大值,即 memo[i][j] = max(memo[i-1][j], memo[i][j-1])。

最后,函数返回 memo[m][n],即两个字符串的最长公共子序列的长度。

请注意,这个实现的时间复杂度是 O(mn),其中 m 和 n 分别是两个输入字符串的长度。这是因为我们需要填充一个 m x n 的表,而每个表项的计算时间是常数时间。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

讯飞星火,代码正常运行:

在Go语言中,我们可以使用动态规划来解决这个问题。以下是实现LCS-LENGTH的带备忘的版本的代码:

package main
import "fmt"

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func lcsLength(X, Y []int) int {
    m := len(X)
    n := len(Y)
    c := make([][]int, m+1)
    for i := range c {
        c[i] = make([]int, n+1)
    }
    for i := 0; i <= m; i++ {
        for j := 0; j <= n; j++ {
            if i == 0 || j == 0 {
                c[i][j] = 0
            } else if X[i-1] == Y[j-1] {
                c[i][j] = c[i-1][j-1] + 1
            } else {
                c[i][j] = max(c[i-1][j], c[i][j-1])
            }
        }
    }
    return c[m][n]
}

func main() {
    X := []int{1, 3, 4, 5, 6, 7, 7, 8}
    Y := []int{2, 3, 5, 6, 7, 8, 9, 10}
    fmt.Println("Length of LCS is", lcsLength(X, Y))
}

在这个程序中,我们首先定义了一个二维数组c,用于存储子问题的解。然后,我们遍历两个输入序列,如果当前位置的元素相同,则该位置的LCS长度为左上角元素的LCS长度加一;否则,该位置的LCS长度为左边元素和上边元素中较大的那个LCS长度。最后,右下角的元素就是整个序列的LCS长度。

在这里插入图片描述

在这里插入图片描述

360智脑,代码不能运行:

以下是一个使用Go语言实现的LCS-Length带备忘的版本:

package main

func lcsLength(memo map[int][int]int, X string, Y string, m int, n int) int {
    if m == 0 || n == 0 {
        return 0
    }

    if v, ok := memo[m][n]; ok {
        return v
    }

    if X[m-1] == Y[n-1] {
        memo[m][n] = lcsLength(memo, X, Y, m-1, n-1) + 1
    } else {
        memo[m][n] = max(lcsLength(memo, X, Y, m-1, n), lcsLength(memo, X, Y, m, n-1))
    }

    return memo[m][n]
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    X := "ABCBDAB"
    Y := "BDCAB"
    memo := make(map[int][int]int)
    m := len(X)
    n := len(Y)
    fmt.Println(lcsLength(memo, X, Y, m, n))
}

在这个代码中,我们使用了一个备忘录(memo)来存储之前计算过的LCS长度。当我们计算LCS长度时,我们首先检查备忘录中是否已经有这个值。如果有,我们就直接返回它;如果没有,我们就计算它,并将其存储在备忘录中。

这个算法的运行时间为O(mn),因为我们在计算LCS长度时,最多需要遍历X和Y字符串的每个字符。

在这里插入图片描述

在这里插入图片描述

智谱清言,代码正常运行:

为了设计一个运行时间为O(mn)的LCS-LENGTH算法,我们可以使用动态规划(DP)技术,并辅以带备忘的递归方法来避免重复计算。在下面给出的Go语言代码中,我们将计算两个字符串
str1

str2
的最长公共子序列的长度。

package main
import (
	"fmt"
)
// LCSLength 计算两个字符串的最长公共子序列的长度
// 使用带备忘的递归方法
func LCSLength(str1, str2 string) int {
	// 创建一个二维切片来保存子问题的结果
	// dp[i][j] 表示str1的前i个字符与str2的前j个字符的LCS长度
	dp := make([][]int, len(str1)+1)
	for i := range dp {
		dp[i] = make([]int, len(str2)+1)
	}
	// 初始化dp数组
	for i := 0; i <= len(str1); i++ {
		for j := 0; j <= len(str2); j++ {
			if i == 0 || j == 0 {
				dp[i][j] = 0
			}
		}
	}
	// 递归计算LCS长度,同时更新dp数组
	return recursiveLCSLength(str1, str2, 0, 0, dp)
}
// recursiveLCSLength 递归计算最长公共子序列的长度
// 同时更新dp数组以避免重复计算
func recursiveLCSLength(str1, str2 string, i, j int, dp [][]int) int {
	// 检查是否已经计算过这个子问题
	if dp[i][j] != 0 {
		return dp[i][j]
	}
	// 如果i或j为0,那么LCS长度为0
	if i == 0 || j == 0 {
		dp[i][j] = 0
		return 0
	}
	// 如果str1[i-1] == str2[j-1],那么当前字符是LCS的一部分
	if str1[i-1] == str2[j-1] {
		dp[i][j] = 1 + recursiveLCSLength(str1, str2, i-1, j-1, dp)
	} else {
		// 否则,取左侧或上侧的LCS长度中的较大值
		dp[i][j] = max(recursiveLCSLength(str1, str2, i-1, j, dp), recursiveLCSLength(str1, str2, i, j-1, dp))
	}
	return dp[i][j]
}
// max 返回两个整数中的较大值
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}
func main() {
	str1 := "ABCBDAB"
	str2 := "BDCAB"
	fmt.Println("The length of LCS is:", LCSLength(str1, str2))
}

这段代码定义了一个
LCSLength
函数,它使用一个二维切片
dp
来存储子问题的解,并且通过递归函数
recursiveLCSLength
来计算最长公共子序列的长度。递归函数会在每次需要计算相同子问题时,检查是否已经在
dp
中存储了结果,以避免重复计算。

main
函数中给出了一个例子,计算字符串"ABCBDAB"和"BDCAB"的最长公共子序列的长度,并打印结果。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

天工,代码正常运行:

Certainly! Below is an implementation of the LCS problem with memoization in Go that runs in O(mn) time, where m and n are the lengths of the two input strings. This implementation uses a二维数组 to store the results of subproblems, avoiding重复计算.

package main

import "fmt"

// LCSMemo computes the length of the longest common subsequence between X and Y using memoization.
func LCSMemo(X, Y string) int {
	m, n := len(X), len(Y)
	dp := make([][]int, m+1)
	for i := range dp {
		dp[i] = make([]int, n+1)
	}

	for i := 1; i <= m; i++ {
		for j := 1; j <= n; j++ {
			if X[i-1] == Y[j-1] {
				dp[i][j] = dp[i-1][j-1] + 1
			} else {
				dp[i][j] = max(dp[i-1][j], dp[i][j-1])
			}
		}
	}

	return dp[m][n]
}

// max returns the larger of two integers.
func max(a, b int) int {
	if a >= b {
		return a
	}
	return b
}

func main() {
	X := "AGGTAB"
	Y := "GXTXAYB"
	length := LCSMemo(X, Y)
	fmt.Printf("Length of LCS: %d\n", length)
}

To use this code, simply run it with the Go runtime and it will output the length of the longest common subsequence between the two given strings. The
max
function is used to handle the comparison of integers, as
int
in Go does not guarantee a specific signedness.

在这里插入图片描述

在这里插入图片描述

灵小智,代码正常运行:

下面是用Go语言编写的带备忘录的最长公共子序列(LCS)问题的代码示例:
package main

import (
	"fmt"
)

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func lcsLengthMemoized(X, Y string, m, n int, memo [][]int) int {
	if m == 0 || n == 0 {
		return 0
	}

	if memo[m][n] != -1 {
		return memo[m][n]
	}

	if X[m-1] == Y[n-1] {
		memo[m][n] = 1 + lcsLengthMemoized(X, Y, m-1, n-1, memo)
		return memo[m][n]
	}

	memo[m][n] = max(lcsLengthMemoized(X, Y, m-1, n, memo), lcsLengthMemoized(X, Y, m, n-1, memo))
	return memo[m][n]
}

func lcsLength(X, Y string) int {
	m := len(X)
	n := len(Y)

	memo := make([][]int, m+1)
	for i := range memo {
		memo[i] = make([]int, n+1)
		for j := range memo[i] {
			memo[i][j] = -1
		}
	}

	return lcsLengthMemoized(X, Y, m, n, memo)
}

func main() {
	X := "ABCBDAB"
	Y := "BDCAB"

	fmt.Printf("Length of LCS: %d\n", lcsLength(X, Y))
}

在这个代码中,我们使用了备忘录(memo)来保存已计算过的子问题的结果,以避免重复计算。通过将已计算过的结果存储在备忘录中,我们可以将算法的运行时间从指数级别降低到线性级别。这样,算法的运行时间为O(mn),其中m和n分别是字符串X和Y的长度。

以上是用Go语言编写的带备忘录的LCS问题的代码示例,希望对你有帮助!

在这里插入图片描述

在这里插入图片描述

chatgpt,代码正常运行:

当设计LCS-LENGTH函数的带备忘的版本时,可以使用动态规划的思想。下面是用Go语言实现的代码:

package main

import (
	"fmt"
)

func lcsLengthMemo(X, Y string) int {
	m := len(X)
	n := len(Y)

	// 创建并初始化备忘录
	memo := make([][]int, m+1)
	for i := 0; i <= m; i++ {
		memo[i] = make([]int, n+1)
		for j := 0; j <= n; j++ {
			memo[i][j] = -1
		}
	}

	var lcs func(int, int) int
	lcs = func(i, j int) int {
		if memo[i][j] != -1 { // 如果已经计算过,直接返回备忘录中的值
			return memo[i][j]
		}
		
		if i == 0 || j == 0 { // 边界条件,其中一个字符串为空
			memo[i][j] = 0
			return 0
		}

		if X[i-1] == Y[j-1] { // 当前字符匹配成功,继续向前匹配
			memo[i][j] = lcs(i-1, j-1) + 1
			return memo[i][j]
		} else { // 当前字符不匹配,取两种情况下的最大值
			left := lcs(i-1, j)
			top := lcs(i, j-1)
			if left > top {
				memo[i][j] = left
			} else {
				memo[i][j] = top
			}
			return memo[i][j]
		}
	}

	return lcs(m, n)
}

func main() {
	X := "ABCBDAB"
	Y := "BDCAB"
	fmt.Println("LCS长度:", lcsLengthMemo(X, Y))
}

该代码使用了备忘录来存储已经计算过的子问题的结果,避免重复计算。通过递归方式进行动态规划,从后往前匹配字符串,并记录最长公共子序列的长度。运行时间复杂度为O(mn)。

在这里插入图片描述

在这里插入图片描述

kimi,代码正常运行:

在 Go 语言中,设计一个带有备忘(Memoization)的 LCS-LENGTH 版本,可以通过创建一个额外的二维切片来存储已经计算过的子问题的解。这样,当我们需要计算一个新的子问题时,我们首先检查这个解是否已经被计算过。如果已经计算过,我们可以直接使用这个解,而不需要重新计算,从而提高算法的效率。

以下是带有备忘的 LCS-LENGTH 函数的 Go 语言实现:

package main

import (
	"fmt"
)

// LCS-LENGTH 函数计算两个序列的最长公共子序列的长度
func LCSLength(x, y []int) int {
	m, n := len(x), len(y)
	memo := make([][]int, m+1)
	for i := range memo {
		memo[i] = make([]int, n+1)
	}

	return lcsLengthMemo(x, y, m, n, memo)
}

// lcsLengthMemo 是 LCS-LENGTH 的递归实现,带有备忘
func lcsLengthMemo(x, y []int, m, n int, memo [][]int) int {
	if m == 0 || n == 0 {
		return 0
	}

	// 检查解是否已经在备忘数组中
	if memo[m][n] != 0 {
		return memo[m][n]
	}

	if x[m-1] == y[n-1] {
		// 如果当前元素相同,递归计算去掉这两个元素后的子问题的解
		memo[m][n] = 1 + lcsLengthMemo(x, y, m-1, n-1, memo)
	} else {
		// 如果当前元素不同,分别计算两种情况的解,并取最大值
		memo[m][n] = max(
			lcsLengthMemo(x, y, m-1, n, memo),
			lcsLengthMemo(x, y, m, n-1, memo),
		)
	}

	return memo[m][n]
}

// max 返回两个整数中的最大值
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	x := []int{1, 0, 0, 1, 0, 1, 0, 1}
	y := []int{0, 1, 0, 1, 1, 0, 1, 1, 0}

	length := LCSLength(x, y)
	fmt.Printf("Length of LCS: %d\n", length)
}

在这个实现中,
LCSLength
函数是用户调用的接口,它初始化备忘数组并调用
lcsLengthMemo
函数。
lcsLengthMemo
函数是递归实现,它使用备忘数组来避免重复计算。这个算法的时间复杂度是 O(m * n),因为每个子问题只计算一次,并且我们使用 O(m * n) 的空间来存储备忘数组。

在这里插入图片描述

在这里插入图片描述

混元,代码正常运行:

带备忘的 LCS-LENGTH 算法是一种动态规划方法,用于计算两个字符串的最长公共子序列的长度。以下是使用 Go 语言实现的带备忘的 LCS-LENGTH 算法:

package main

import (
	"fmt"
)

func lcsLengthMemo(x, y string, m, n int, memo [][]int) int {
	if m == 0 || n == 0 {
		return 0
	}

	if memo[m][n] != -1 {
		return memo[m][n]
	}

	if x[m-1] == y[n-1] {
		memo[m][n] = 1 + lcsLengthMemo(x, y, m-1, n-1, memo)
	} else {
		memo[m][n] = max(lcsLengthMemo(x, y, m-1, n, memo), lcsLengthMemo(x, y, m, n-1, memo))
	}

	return memo[m][n]
}

func lcsLength(x, y string) int {
	m, n := len(x), len(y)
	memo := make([][]int, m+1)
	for i := range memo {
		memo[i] = make([]int, n+1)
		for j := range memo[i] {
			memo[i][j] = -1
		}
	}

	return lcsLengthMemo(x, y, m, n, memo)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	x := "ABCBDAB"
	y := "BDCAB"
	fmt.Printf("The length of the LCS is: %d\n", lcsLength(x, y))
}

这个实现首先创建一个二维数组
memo
用于存储已经计算过的子问题的解。
lcsLengthMemo
函数是一个递归函数,用于计算两个字符串的最长公共子序列的长度。当递归到基本情况时,它会返回 0。如果当前子问题已经计算过,它会直接返回已经计算的结果。否则,它会计算当前子问题的解,并将其存储在备忘录中,然后返回解。

lcsLength
函数是一个包装函数,用于初始化备忘录并调用
lcsLengthMemo
函数。最后,
main
函数调用
lcsLength
函数并打印结果。

在这里插入图片描述

在这里插入图片描述

大家好,我是沙漠尽头的狼!

AvaloniaUI是一个强大的跨平台.NET客户端开发框架,让开发者能够针对Windows、Linux、macOS、Android和iOS等多个平台构建应用程序。在构建复杂的应用程序时,模块化和组件间的通信变得尤为重要。Prism框架提供了模块化的开发方式,支持插件的热拔插,而MediatR则是一个实现了中介者(Mediator)模式的事件订阅发布框架,非常适合用于模块之间以及模块与主程序之间的通信。

本文重点是介绍
MediatR
,它 是
.NET
中的开源简单中介者模式实现。它通过一种进程内消息传递机制(无其他外部依赖),进行请求/响应、命令、查询、通知和事件的消息传递,并通过泛型来支持消息的智能调度。开源库地址是
https://github.com/jbogard/MediatR。

本文将详细介绍如何在Avalonia项目中使用MediatR和Microsoft的依赖注入(MS.DI)库来实现事件驱动的通信。

0. 基础知识准备-MediatR的基本用法

MediatR
中有两种消息传递的方式:

  • Request/Response
    ,用于一个单独的Handler。
  • Notification
    ,用于多个Handler。

Request/Response

Request/Response
有点类似于 HTTP 的 Request/Response,发出一个 Request 会得到一个 Response。

Request
消息在 MediatR 中,有两种类型:

  • IRequest<T>
    返回一个T类型的值。
  • IRequest
    不返回值。

对于每个 request 类型,都有相应的 handler 接口:

  • IRequestHandler<T, U>
    实现该接口并返回
    Task<U>
  • RequestHandler<T, U>
    继承该类并返回
    U
  • IRequestHandler<T>
    实现该接口并返回
    Task<Unit>
  • AsyncRequestHandler<T>
    继承该类并返回
    Task
  • RequestHandler<T>
    继承该类不返回

Notification

Notification
就是通知,调用者发出一次,然后可以有多个处理者参与处理。

1. 准备工作

首先,确保你的Avalonia项目中已经安装了必要的NuGet包。你将需要
Prism.DryIoc.Avalonia
作为依赖注入容器,以及
MediatR
来处理事件的发布和订阅。此外,为了将MediatR集成到DryIoc容器中,你还需要
DryIoc.Microsoft.DependencyInjection
包(这里感谢网友

提供的技术解答)。

在项目的
.csproj
文件或NuGet包管理器中添加以下引用:

<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11072" /> 
<PackageReference Include="MediatR" Version="12.2.0" />  
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="8.0.0-preview-01" />

2. 配置容器和注册服务

在Avalonia项目中,你需要配置DryIoc容器以使用Microsoft的DI扩展,并注册MediatR服务。这通常在你的主启动类(如
App.axaml.cs
)中完成。

以下是配置容器和注册服务的示例代码:

namespace CodeWF.Tools.Desktop;

public class App : PrismApplication
{
    // 省略了模块注入等和主题无关的代码,有兴趣源码在文末可查

    /// <summary>
    /// 1、DryIoc.Microsoft.DependencyInjection低版本可不要这个方法(5.1.0及以下)
    /// 2、高版本必须,否则会抛出异常:System.MissingMethodException:“Method not found: 'DryIoc.Rules DryIoc.Rules.WithoutFastExpressionCompiler()'.”
    /// 参考issues:https://github.com/dadhi/DryIoc/issues/529
    /// </summary>
    /// <returns></returns>
    protected override Rules CreateContainerRules()
    {
        return Rules.Default.WithConcreteTypeDynamicRegistrations(reuse: Reuse.Transient)
            .With(Made.Of(FactoryMethod.ConstructorWithResolvableArguments))
            .WithFuncAndLazyWithoutRegistration()
            .WithTrackingDisposableTransients()
            //.WithoutFastExpressionCompiler()
            .WithFactorySelector(Rules.SelectLastRegisteredFactory());
    }

    protected override IContainerExtension CreateContainerExtension()
    {
        IContainer container = new Container(CreateContainerRules());
        container.WithDependencyInjectionAdapter();

        return new DryIocContainerExtension(container);
    }

    protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
    {
        base.RegisterRequiredTypes(containerRegistry);

        IServiceCollection services = ConfigureServices();

        IContainer container = ((IContainerExtension<IContainer>)containerRegistry).Instance;

        container.Populate(services);
    }

    private static ServiceCollection ConfigureServices()
    {
        var services = new ServiceCollection();

        // 注入MediatR
        var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

        // 添加模块注入,未显示调用模块类型前,模块程序集是未加载到当前程序域`AppDomain.CurrentDomain`的
        var assembly = typeof(SlugifyStringModule).GetAssembly();
        assemblies.Add(assembly);
        services.AddMediatR(configure =>
        {
            configure.RegisterServicesFromAssemblies(assemblies.ToArray());
        });

        return services;
    }
}

在上面的代码中,我们重写了
CreateContainerRules

CreateContainerExtension

RegisterRequiredTypes
方法以配置DryIoc容器,并注册了MediatR服务和相关处理程序。

注意,在注册MediatR服务时,我们从当前已加载的程序集列表中查找并注册处理程序。如果模块是按需加载的,请确保在注册处理程序之前已加载了相应的模块。

此外,我们还演示了如何手动添加模块程序集到列表中以便注册处理程序。这通常在你需要显式控制哪些模块和处理程序被注册时很有用。但是,请注意,在大多数情况下,你可能希望使用更自动化的方式来加载和注册模块及处理程序(例如,通过扫描特定目录或使用约定等)。这取决于你的具体需求和项目结构。

另外,请注意代码中的注释和说明,它们提供了有关每个步骤和配置的额外信息。在实际项目中,你可能需要根据项目的实际情况和需求进行相应的调整和优化。例如,你可能需要处理循环依赖、配置作用域、使用拦截器或装饰器等高级功能。这些都可以在DryIoc和MediatR的文档中找到更详细的说明和示例。

3. MediatR2种传递方式

有了前面的基础知识准备,我们添加类库工程
CodeWF.Tools.MediatR.Notifications
,并添加请求定义(主工程及模块的响应处理程序需要实现):

public class TestRequest : IRequest<string>
{
    public string? Args { get; set; }
}

添加通知定义:

public class TestNotification : INotification
{
    public string? Args { get; set; }
}

请求和通知定义结构一样(实现接口不同),只有一个字符串属性。

4. 添加处理程序

示例工程结构如下,因为该开源项目(文末链接)写在站长的AvaloniaUI桌面工具工程,本文只关注如下图3个工程即可:

程序结构

在AvaloniaUI主工程(CodeWF.Tools.Desktop)添加请求响应处理程序:

public class TestHandler : IRequestHandler<TestRequest, string>
{
    public async Task<string> Handle(TestRequest request, CancellationToken cancellationToken)
    {
        return await Task.FromResult($"主工程处理程序:Args = {request.Args}, Now = {DateTime.Now}");
    }
}

添加通知响应处理程序:

public class TestNotificationHandler(INotificationService notificationService) : INotificationHandler<TestNotification>
{
    public Task Handle(TestNotification notification, CancellationToken cancellationToken)
    {
        notificationService.Show("Notification",
            $"主工程Notification处理程序:Args = {notification.Args}, Now = {DateTime.Now}");
        return Task.CompletedTask;
    }
}

在模块【CodeWF.Tools.Modules.SlugifyString】中添加请求响应处理程序(因为顺序关系,不会触发,这里添加只是演示请求为一对一响应):

public class TestHandler : IRequestHandler<TestRequest, string>
{
    public async Task<string> Handle(TestRequest request, CancellationToken cancellationToken)
    {
        return await Task.FromResult($"模块【SlugifyString】Request处理程序:Args = {request.Args}, Now = {DateTime.Now}");
    }
}

添加通知响应处理程序(会和主工程通知响应处理程序一样被触发):

public class TestNotificationHandler(INotificationService notificationService) : INotificationHandler<TestNotification>
{
    public Task Handle(TestNotification notification, CancellationToken cancellationToken)
    {
        notificationService.Show("Notification",
            $"模块【SlugifyString】Notification处理程序:Args = {notification.Args}, Now = {DateTime.Now}");
        return Task.CompletedTask;
    }
}

几个响应处理程序类定义类似:收到请求时,返回格式化字符串;收到通知时,弹出提示表明当前是哪个位置收到的通知,便于演示效果。

5. 请求和通知演示

触发操作我们写在模块【CodeWF.Tools.Modules.SlugifyString】中,在模块的ViewModel类里通过依赖注入获取请求和通知的发送者实例ISender和IPublisher:

using Unit = System.Reactive.Unit;

namespace CodeWF.Tools.Modules.SlugifyString.ViewModels;

public class SlugifyViewModel : ViewModelBase
{
    // 省略别名转换相关逻辑代码,源码文末查看
    
    private readonly INotificationService _notificationService;
    private readonly IClipboardService? _clipboardService;
    private readonly ITranslationService? _translationService;

    public SlugifyViewModel(INotificationService notificationService, IClipboardService clipboardService,
        ITranslationService translationService, ISender sender, IPublisher publisher) : base(sender, publisher)
    {
        _notificationService = notificationService;
        _clipboardService = clipboardService;
        _translationService = translationService;
        KindChanged = ReactiveCommand.Create<TranslationKind>(OnKindChanged);
    }


    public async Task ExecuteMediatRRequestAsync()
    {
        var result = Sender.Send(new TestRequest() { Args = To });
        _notificationService.Show("MediatR", $"收到响应:{result.Result}");
    }

    public async Task ExecuteMediatRNotificationAsync()
    {
        await Publisher.Publish(new TestNotification() { Args = To });
    }

}

点击
测试MediatR-Request
按钮触发调用
ISender.Send
发出请求并得到响应,通过点击
测试MediatR-Notification
按钮触发调用
IPublisher.Publish
发出通知。

请求效果:

看上面的请求效果:虽然在主工程和模块工程都注册了一个响应,但只有主工程被触发。

通知效果:

在主工程和模块工程都注册了一个通知响应,所以两个处理程序都弹出了提示。

6. 总结

为什么使用MediatR,而未使用Prism的事件聚合器?

站长开发工具做了在线版(
https://blazor.dotnet9.com
),也做了跨平台桌面版本(AvaloniaUI),两个版本使用MediatR可以复用大部分事件代码。

CQRS or DDD?

这节直接复制
MediatR 在 .NET 应用中的实践 - 明志唯新 (yimingzhi.net)
,大家应该可以学到些什么:

软件开发发展到今天,模式和理念不断在架构中刷新:从分布式到微服务,再到云原生 ……。时代对一个程序员,尤其是服务端程序员,提出的要求越来越高。
DDD
(领域驱动设计)在微服务架构中一再被提及,甚至有人提出这是必须项!

实施一个完美的 DDD 还是有难度的,现实中奋战在一线的
CRUD
程序员还是不少。那么在 CRUD 和 DDD 之间我们是否还有缓冲区呢?MediatR 的作者曾经也撰文讨论过这个问题,我很认同他的基本观点:设计是为应用服务的,不能为了 DDD 而 DDD。

CQRS
的全称是:"Command and Query Responsibility Segregation",直译过来就是命令与查询责任分离,可以通俗的理解为
读写分离

微软的官方文档中对此做过如下陈述:

CQRS 命令和查询责任分离数据存储的读取和更新操作分离的模式。 在应用程序中实现 CQRS 可以最大程度地提高其性能、可伸缩性和安全性。 通过迁移到 CQRS 而创建的灵活性使系统能够随着时间的推移更好地发展,并防止更新命令在域级别导致合并冲突。

微软也给出了相应的隔离模型解决方案:

CQRS 使用命令来更新数据,使用查询来读取数据,将读取和写入 分离到不同的 模型中。

  • 命令应基于任务,而不是以数据为中心。
  • 命令可以放置在队列中进行异步处理,而不是同步处理。
  • 查询从不修改数据库。 查询返回的 DTO 不封装任何域知识。

CQRS 的好处包括:

  • 独立缩放
    : CQRS 允许读取和写入工作负载独立缩放,这可能会减少锁争用。
  • 优化的数据架构
    : 读取端可使用针对查询优化的架构,写入端可使用针对更新优化的架构。
  • 安全性
    : 更轻松地确保仅正确的域实体对数据执行写入操作。
  • 关注点分离
    : 分离读取和写入端可使模型更易维护且更灵活。 大多数复杂的业务逻辑被分到写模型。 读模型会变得相对简单。
  • 查询更简单
    : 通过将具体化视图存储在读取数据库中,应用程序可在查询时避免复杂联接。

有了
MediatR
我们可以在应用中轻松实现
CQRS
:

  • IRequest<>
    的消息名称以
    Command
    为结尾的是命令,其对应的 Handler 执行

    任务
  • IRequest<>
    的消息名称以
    Query
    为结尾的是查询,其对应的 Handler 执行

    数据

结束语

MediatR
是一个简单的中介者实现,可以极大降低我们的应用复杂度,也能够使得我们一路从
CRUD

CQRS

DDD
进行逐级演进。毕竟我们是生活在现实中的人,不能罔顾商业现实,纯粹一味追求技术。

商业技术的演进,应该是一路持续的改革而不是来一场革命。疫情总有反复,但是我们得活着,相对轻松的活着!

参考

文中示例写了主要代码,但可能缺失部分细节,源码链接如下,欢迎留言交流。

参考文章:
MediatR 在 .NET 应用中的实践

本文源码:
Github

一、前言

  • 本篇文章是『Echarts』文章的第 2 篇,主要介绍『Echarts』基本使用

在『Echarts』第 1 篇文章中,我们介绍了 Echarts 的概述及其强大的数据可视化功能。本篇将继续深入,重点带您了解 Echarts 的基本使用方法,包括如何快速安装、配置以及绘制简单的图表。

二、安装『Echarts』

请首先访问 Echarts 官方网站,网址为:
https://echarts.apache.org/zh/index.html。成功打开网页后,您需要在页面左上角找寻
“下载” 选项,点击它即可进入下载页面。

点击后系统会自动跳转至 Echarts 的下载页面。在该页面中,您可以根据需求挑选合适的版本。建议下载最新版 “Echarts 5.4.3”,以确保获得最佳性能和最新功能。之后,点击 “Dist(GitHub)”,即可开始下载所选版本。

首先点击链接,您将被引导至 GitHub 官方网站。在那里,请找到
echarts.js

echarts.min.js
这两个重要文件,并分别进行下载。这两个文件构成了 Echarts 核心组件,它们是运行 Echarts 所必需的。

首先,定位并点击您想要下载的文件以进入其详情页面;然后,寻找并点击页面中的 “下载” 按钮;最后,文件便会开始下载并保存至您的本地设备。

echarts.min.js 同理可证,这里就不再赘述了。

我们既可以选择直接使用相关文件,也可以通过 npm 进行安装。需要区分的是,文件分为压缩版和未压缩版两种形式:压缩版体积小,加载快速,适合生产环境使用;而未压缩版体积较大,包含完整的代码结构,便于开发时调试。通常,我们建议在日常开发中使用未压缩版以方便调试,在项目部署时转而使用体积更小的压缩版。在此,我们决定直接使用已下载的文件,并将其复制到项目目录中以便使用:

至此,Echarts 的安装步骤已全部完成。下一阶段,我们将进入 Echarts 的配置过程,并教您如何创建您的第一个图表。请跟随接下来的指导继续操作。

三、使用『Echarts』

在您的项目中集成 Echarts 之前,必须先引入必要的文件。这里我们选用 echarts.js,它囊括了完整的代码构架,便于后续的调试工作。具体步骤如下:在 HTML 文件中插入对 echarts.js 文件的引用,代码示例如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Echarts 基本使用</title>
    <!--
    1.导入 Echarts 插件
    -->
    <script src="js/echarts.js"></script>
</head>
<body>
<script>
</script>
</body>
</html>

成功引入 echarts.js 文件之后,我们便可以着手制作图表。首先,需要在 HTML 文档的
<body>
标签内添加一个
<div>
容器,这个容器将用于展示图表。具体实现代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Echarts基本使用</title>
    <!--
    1.导入Echarts插件
    -->
    <script src="js/echarts.js"></script>
</head>
<body>
<!--
2.为Echarts准备一个容器
这个容器的作用就是将来用来将图标绘制到什么地方
-->
<div style="width: 600px;height: 400px;" id="app"></div>

<script>
</script>
</body>
</html>


<div>
标签中,我们设定了容器的宽度为 600 像素,高度为 400 像素。该容器将用来展示图表,因此我们为其指定了一个 id 属性,其值为 “app”。这一 id 属性会在后续代码中使用,所以请确保记住它。

接下来我们需要获取已经准备好的容器,并通过
echarts.init()
方法在该容器中初始化一个 ECharts 实例。具体代码如下:

<script>
    // 省略其它代码

    /*
    4.创建一个Echarts对象
    通过Echarts对象的init方法来创建
    init 方法需要传递一个参数,这个参数就是我们的容器

    将来 echarts 绘制出来的图表就会绘制到 oDiv 这个容器上
    * */
    let myChart = echarts.init(oDiv);
</script>

初始化了 ECharts 实例之后,接下来要做的事情就是对 oDiv 进行配置。具体是什么配置呢?例如,配置您要绘制什么类型的图表,您的图表是否有标题,您的图表包含哪些数据,这些数据在 X 轴上是什么,在 Y 轴上是什么,这些都是需要配置的。那么这个配置是怎么做的呢?我们可以通过
setOption()
方法来进行配置。首先定义一个对象,然后只需要在这个对象中设置配置项即可。

首先暂缓介绍配置部分,后续将继续深入。现在,我将代码示例贴出,对于配置对象中的各项属性,暂时不做详细说明。

<script>
    /*
    3.拿到准备好的容器
    * */
    let oDiv = document.querySelector('div');

    /*
    4.创建一个Echarts对象
    通过Echarts对象的init方法来创建
    init 方法需要传递一个参数,这个参数就是我们的容器

    将来 echarts 绘制出来的图表就会绘制到 oDiv 这个容器上
    * */
    let myChart = echarts.init(oDiv);

+    let option = {
+
+    }
+
+    // 将配置传递给Echarts对象
+    myChart.setOption(option);
</script>

经过上述代码编写,以后若想进行图表绘制,其实存在几个固定步骤,具体包括:导入 ECharts,为 ECharts 创建一个容器,获取已准备好的容器,创建一个 ECharts 实例,为 ECharts 实例撰写配置项,将配置项传递给 ECharts。在这些步骤中,唯一的不同在于撰写 ECharts 实例的配置项。

鉴于我们要用 ECharts 展示的数据多样性,以及图表类型的多变性,使用 ECharts 时的核心学习重点就是其配置选项。掌握好配置,即可灵活运用 ECharts 进行数据可视化展示。

四、配置『Echarts』

关于 ECharts 的配置,不免要先参考官方文档。在官方网站左上角可以找到 “文档” 选项;在该选项中有 “使用手册”,点击即可进入查阅。

向下滑动即可查看官方提供的一些配置选项,我没有过多考虑便直接复制到代码中,以便先行观察效果。

通过浏览器访问 index.html,即可查看展示效果:

发现屏幕上显示了一个柱状图,如果需要更换其他类型的图表,可以简单地修改配置对象中的
series
属性。例如,将
series
属性中的某个元素的
type
属性值改为 "line",就能将柱状图变更为折线图。

let option = {
    title: {
        text: 'ECharts 入门示例'
    },
    legend: {
        data: ['销量']
    },
    xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
    },
    yAxis: {},
    series: [
        {
            name: '销量',
-            type: 'bar',
+            type: 'line',
            data: [5, 20, 36, 10, 10, 20]
        }
    ]
}

通过浏览器访问 index.html,即可查看展示效果:

至此,我们结束了对 ECharts 的使用探索。下面,我们将详细审视配置对象中的各项内容。

五、配置项详解

1. title

设置图表的标题

首先让我们关注图表左上角的标题,它显示为 “ECharts 入门示例”。这个标题是如何设置的呢?实际上,在配置对象中有一个
title
属性,它包含一个对象。在这个对象里,有一个名为
text
的属性,其值正是标题所展示的内容。通过更改
text
属性的值,我们便能轻松修改标题文本。

2. legend

设置图表的图例

这个 legend 有什么作用呢,这个 legend 其实是叫图例,图例的作用是什么呢?它在图表中起到关键的说明作用,位于图表顶部,通过对符号、色块或线型的标注,帮助我们更好地理解图表内容。

图例的功能是点击后可以控制图表的显示与隐藏。一个图表中可以包含多个图例。现在,我将对代码进行改造,这部分工作的具体含义暂时不做讨论。

let option = {
    title: {
        text: 'ECharts 入门示例'
    },
    legend: {
-        data: ['销量']
+        data: ['销量', '产量']
    },
    xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
    },
    yAxis: {},
    series: [
        {
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20]
        },
+        {
+            name: '产量',
+            type: 'bar',
+            data: [50, 120, 130, 60, 40, 80]
+        }
    ]
}

代码编写已完成,现在让我们查看一下效果。通过浏览器打开 index.html 文件,即可预览展示的效果:

图表目前展示两根柱状,代表不同的销量与产量,且颜色区分。图例中默认展示销量和产量。若需单独查看产量,只需点击图例中的 “销量” 即可隐藏销量柱状;反之,想单独观察销量时,点击 “产量” 隐藏产量柱状即可。

3. xAxis

设置图表 X 轴上显示的数据

4. yAxis

设置图表 Y 轴上显示的数据

在我们的代码中,如果没有明确设置 Y 轴的数据,系统会自动根据提供的数据集来计算并填充 Y 轴上显示的数据。

5. series

设置图表的数据

该属性接受一个对象数组,数组中的每个对象代表一类数据。在您提供的代码示例中,数组包含两个对象,意味着有两类数据。因此,页面上显示的柱状图由这两类数据绘制而成,并用不同颜色加以区分。另外,我们还需要深入理解数组中各对象的属性及其含义。

  • name

用来配置当前数据项的名称与图表图例设置中的名称完全相同,以便图例可以正确地控制对应的数据显示。

  • type

配置绘制数据时需指定图表类型:若设定为
bar
,则表示创建柱状图;若为
line
,则表示生成折线图。

  • data

配置数据值由一个数组定义,也就是对应的数据简单的来说,其中每个元素对应一个数据点。举例来说,
data: [5, 20, 36, 10, 10, 20]
表示有六个数据点,其数值依次为 5、20、36、10、10 和 20。

六、总结

通过本文的学习,您将能够掌握以下核心知识点:

  • 1.ECharts 的安装过程;
  • 2.ECharts 的基本使用方法
  • 3.对 ECharts 进行个性化配置的技巧
  • 4.对 ECharts 配置项的深入解析
  • 如果您认为本文有价值,并且对您有所裨益,不妨点赞、收藏或转发分享。您的每一个支持都是我创作的不竭动力,让我们共同分享知识的喜悦!