2024年9月

概述

跳跃表(SkipList)是链表加多级索引组成的数据结构。链表的数据结构的查询复条度是 O(N)。为了提高查询效率,可以在链表上加多级索引来实现快速查询。跳跃表不仅能提高搜索性能。也能提高插入和删除操作的性能。索引的层数也叫作跳跃表的高度


查找

在跳跃表的结构中会首先从顶层开始查找,当顶层不存在时向下一层查找,重复此查找过程直到跳跃到原始链表。如图所示,在 [1,3,4,10,1,20] 的链表中查找 10,首先从二级索引中查找,由于 1 和 4 都比 10 小,因此接着在一级索引查找,由于 10 大于 4 小于 11,因此接着向下查找,原始链表中 4 的下一个节点 10 便是需要查找的数据


插入

首先按照查找流程找到待插入元素的前驱和后继,然后按照随机算法生成一个高度值 height,最后将待插入的节点按照高度值生成一个垂直节点(这个节点的层数正好等于高度值),并将其插人跳跃表的多条链表中。如果高度值 heigh 大于插入前跳跃表的高度,那么跳跃表的高度被提升为 height,同时需要更新头节点和尾节点的指针指向。如果 height 小于或等于跳跃表的高度,那么需要更新待插入元素的前驱和后继的指针指向。在 [1,3,4.10,11,20] 的跳跃表中插入 18 的过程如图所示


删除

在删除节点时首先需要找到待删除的节点在每一层的前驱和后继,接着将其前驱节点的后继替换为待删除的节点的后继,删除 4 的过程如图所示


代码实现

import java.util.Random;
import java.util.Stack;

class SkipNode<T> {

    int key;
    T value;
    SkipNode right,down;//左右上下四个方向的指针
    public SkipNode (int key,T value) {
        this.key=key;
        this.value=value;
    }

}

public class SkipList <T> {

    SkipNode headNode;//头节点,入口
    int highLevel;//层数
    Random random;// 用于投掷硬币
    final int MAX_LEVEL = 32;//最大的层
    
    SkipList(){
        random=new Random();
        headNode=new SkipNode(Integer.MIN_VALUE,null);
        highLevel=0;
    }
    
    public SkipNode search(int key) {
        SkipNode team=headNode;
        while (team!=null) {
            if(team.key==key){
                return  team;
            }
            else if(team.right==null) { //右侧没有了,只能下降
                team=team.down;
            }
            else if(team.right.key>key) { //需要下降去寻找
                team=team.down;
            }
            else { //右侧比较小向右
                team=team.right;
            }
        }
        return null;
    }

    public void delete(int key) { //删除不需要考虑层数
        SkipNode team=headNode;
        while (team!=null) {
            if (team.right == null) { //右侧没有了,说明这一层找到,没有只能下降
                team=team.down;
            }
            else if(team.right.key==key) { //找到节点,右侧即为待删除节点
                team.right=team.right.right;//删除右侧节点
                team=team.down;//向下继续查找删除
            }
            else if(team.right.key>key) { //右侧已经不可能了,向下
                team=team.down;
            }
            else { //节点还在右侧
                team=team.right;
            }
        }
    }
    
    public void add(SkipNode node) {
    
        int key=node.key;
        SkipNode findNode=search(key);
        if(findNode!=null) { //如果存在这个key的节点
        
            findNode.value=node.value;
            return;
        }

        Stack<SkipNode>stack=new Stack<SkipNode>();//存储向下的节点,这些节点可能在右侧插入节点
        SkipNode team=headNode;//查找待插入的节点   找到最底层的哪个节点。
        while (team!=null) {//进行查找操作
            if(team.right==null) { //右侧没有了,只能下降
                stack.add(team);//将曾经向下的节点记录一下
                team=team.down;
            }
            else if(team.right.key>key) { //需要下降去寻找
                stack.add(team);//将曾经向下的节点记录一下
                team=team.down;
            }
            else { //向右
                team=team.right;
            }
        }

        int level=1;//当前层数,从第一层添加(第一层必须添加,先添加再判断)
        SkipNode downNode=null;//保持前驱节点(即down的指向,初始为null)
        while (!stack.isEmpty()) {
            //在该层插入node
            team=stack.pop();//抛出待插入的左侧节点
            SkipNode nodeTeam=new SkipNode(node.key, node.value);//节点需要重新创建
            nodeTeam.down=downNode;//处理竖方向
            downNode=nodeTeam;//标记新的节点下次使用
            if(team.right==null) {//右侧为null 说明插入在末尾
                team.right=nodeTeam;
            }
            //水平方向处理
            else {//右侧还有节点,插入在两者之间
                nodeTeam.right=team.right;
                team.right=nodeTeam;
            }
            //考虑是否需要向上
            if(level>MAX_LEVEL)//已经到达最高级的节点啦
                break;
            double num=random.nextDouble();//[0-1]随机数
            if(num>0.5)//运气不好结束
                break;
            level++;
            if(level>highLevel) { //比当前最大高度要高但是依然在允许范围内 需要改变head节点
                highLevel=level;
                //需要创建一个新的节点
                SkipNode highHeadNode=new SkipNode(Integer.MIN_VALUE, null);
                highHeadNode.down=headNode;
                headNode=highHeadNode;//改变head
                stack.add(headNode);//下次抛出head
            }
        }
    }
    
    public void printList() {
        SkipNode teamNode=headNode;
        int index=1;
        SkipNode last=teamNode;
        while (last.down!=null){
            last=last.down;
        }
        while (teamNode!=null) {
            SkipNode enumNode=teamNode.right;
            SkipNode enumLast=last.right;
            System.out.printf("%-8s","head->");
            while (enumLast!=null&&enumNode!=null) {
                if(enumLast.key==enumNode.key) {
                    System.out.printf("%-5s",enumLast.key+"->");
                    enumLast=enumLast.right;
                    enumNode=enumNode.right;
                }
                else {
                    enumLast=enumLast.right;
                    System.out.printf("%-5s","");
                }
            }
            teamNode=teamNode.down;
            index++;
            System.out.println();
        }
    }
}

模板方法模式(Template Method Pattern)也称之为模板模式(Template Pattern),是设计模式中最简单的模式之一。

先来看定义:
定义一个操作中算法的骨架(模板),将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重新定义算法某些特定的步骤。
这个定义还是有一些晦涩,我的理解是这样的:(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )
在父类中我们可以定义一块业务的整体实现过程,但是针对某些步骤的具体实现逻辑,我们可以暂时先只定义一个抽象方法,在未来定义子类的过程中,实现/重写该方法。
这个模式主要是为了解决,很多场景中,我们并不知道未来实际使用中,具体需要怎么实现,甚至会出现多个具体实现,针对此,我们可以先定义父类中已经明确的业务。
大致的调用结构如下:

它是面向对象的23种设计模式中的一种,属于行为模式的范围。
来看示例代码:

音乐播放器抽象类

1 packagecom.example.demo.learn.pattern.behavior.templatemethod;2 
3 importlombok.extern.slf4j.Slf4j;4 
5 /**
6 * @discription7  */
8 @Slf4j9 public abstract classAbstractMusicPlayer {10     public voidstartUp() {11 showFrame();12 doCustomizedOpt();13 }14 
15     protected abstract voidplayWelcomeMsg();16 
17     protected voidshowFrame() {18 showMainFrame();19 playWelcomeMsg();20 }21 
22     protected abstract voiddoCustomizedOpt();23 
24 
25     protected abstract voidshowMainFrame();26 }

酷猫播放器

1 packagecom.example.demo.learn.pattern.behavior.templatemethod;2 
3 importlombok.extern.slf4j.Slf4j;4 
5 /**
6 * @discription7  */
8 @Slf4j9 public class CoolCatPlayer extendsAbstractMusicPlayer{10 @Override11     protected voidplayWelcomeMsg() {12       log.warn("hi man");13 }14 
15 @Override16     protected voiddoCustomizedOpt() {17         log.warn("您有一份价值99元的免费礼品待领取,快点击下方链接");18 }19 
20 @Override21     protected voidshowMainFrame() {22         log.warn("打开酷猫音乐主界面");23 }24 }

酷他音乐盒

1 packagecom.example.demo.learn.pattern.behavior.templatemethod;2 
3 importlombok.extern.slf4j.Slf4j;4 
5 /**
6 * @discription7  */
8 @Slf4j9 public class CoolHePlayer extendsAbstractMusicPlayer {10 @Override11     protected voidplayWelcomeMsg() {12         log.warn("欢迎来到酷他音乐盒");13 }14 
15 @Override16     protected voiddoCustomizedOpt() {17         log.warn("一刀999,和兄弟一起战个痛快");18 }19 
20 @Override21     protected voidshowMainFrame() {22         log.warn("打开酷他音乐盒主界面");23 }24 }

执行主类

1 packagecom.example.demo.learn.pattern.behavior.templatemethod;2 
3 /**
4 * @discription5  */
6 
7 public classPatternMain {8     public static voidmain(String[] args) {9         AbstractMusicPlayer coolCat = newCoolCatPlayer();10 coolCat.startUp();11 
12         AbstractMusicPlayer coolHe = newCoolHePlayer();13 coolHe.startUp();14 }15 }

输出如下

15:38:12.515 [main] WARN com.example.demo.learn.pattern.behavior.templatemethod.CoolCatPlayer -打开酷猫音乐主界面15:38:12.518 [main] WARN com.example.demo.learn.pattern.behavior.templatemethod.CoolCatPlayer -hi man15:38:12.518 [main] WARN com.example.demo.learn.pattern.behavior.templatemethod.CoolCatPlayer -您有一份价值99元的免费礼品待领取,快点击下方链接15:38:12.518 [main] WARN com.example.demo.learn.pattern.behavior.templatemethod.CoolHePlayer -打开酷他音乐盒主界面15:38:12.518 [main] WARN com.example.demo.learn.pattern.behavior.templatemethod.CoolHePlayer -欢迎来到酷他音乐盒15:38:12.518 [main] WARN com.example.demo.learn.pattern.behavior.templatemethod.CoolHePlayer - 一刀999,和兄弟一起战个痛快

我们定义了一个播放器的类,并且约定了播放器启动时,我们需要具体做的业务:

类图

步骤1、打开主界面

步骤2、做一些定制的用户操作
但是有多个播放器(如酷猫音乐、酷他音乐盒),他们的主界面和用户定制操作都各有不同,因此我们可以先只定义以上操作的抽象方法,对于具体操作的实现留给子类完成即可。

模板方法核心的步骤就是2点:
父类中定义骨架(模板),组织各种定义好的抽象方法
子类根据实际业务实现抽象方法

前文回顾


在上篇文章中,我们约定了一种衡量格子价值的方式,如下表。

综合价值排序 己方价值 敌方价值 对应的奖励数值
1 Lv1 ? \(2^{20}\)
2 ? Lv1 \(2^{16}\)
3 Lv2 ? \(2^{12}\)
4 Lv2 \(2^{8}\)
5 Lv3 \(2^{4}\)
6 Lv4 \(2^{0}\)

在该表中,对不同的情形,设计了不同的奖励数值,这些数值大多是采用经验公式,人为估计的数值,并不是最优良的数值。同样的,在上表中的除前两类为,其余都可根据实际情况进一步的细分权重,这里给出一个样例供大家参考/理解:

综合价值排序 己方价值 敌方价值 对应的奖励数值
3.1 Lv2 Lv2 \(2^{13}\)
3.2 Lv2 Lv3 \(2^{12}\)
3.3 Lv2 Lv4 \(2^{11}\)

同样是能构成杀招(Lv2等级),能顺便堵死对面杀招/优良的位置自然是更好的。

在附录中给出了详细的权重表

本篇中我们将基于遗传算法讨论如何让AI学习奖励值。

遗传算法概述


遗传算法(Genetic Algorithm, GA)是一种模拟自然选择和遗传机制的优化算法。它用于寻找问题的最优解,特别适用于复杂的优化问题和搜索问题。遗传算法基于达尔文的自然选择理论,通过模拟生物进化过程来逐步改进解决方案。

遗传算法的基本步骤如下:

  1. 初始化:创建一个初始种群,种群中的每一个个体(通常称为染色体或解)是问题的一个潜在解。

  2. 评估:计算种群中每个个体的适应度值,这通常通过一个目标函数来进行,适应度值表示个体的优劣。

  3. 选择:根据适应度值选择个体进行繁殖,优良个体有更高的概率被选择,以生成下一代种群。

  4. 交叉(Crossover):通过将两个个体的部分基因交换,生成新的个体。这一步模仿了生物的交配过程,可以产生新的解。

  5. 变异(Mutation):在某些个体中随机改变基因,以引入新的基因变异。这一步帮助算法跳出局部最优解,增加解的多样性。

  6. 替换:将一部分个体替换为最优良的个体,保留最优秀的基因,使得种群的型状不会出现下降或震荡。

  7. 终止:判断算法是否满足终止条件,如达到最大迭代次数或找到足够好的解。如果满足条件,算法终止,返回最优解;否则,返回第2步。

遗传算法实现思路

初始化


本文所设计的AI决策方案共包含12个参数,其中11个是奖励权重
\(R_i\)
,1个是对劣质选项接受度
\(K\)

我们可以定义
\(N\)
个智能体,分别用初始权重进行初始化,一般来说,
\(N\)
可以取10~100,最好选择偶数,否则会有一些不必要的麻烦。

初始化过程可以用数学公式表示为:

\[W_i^{t=0} = W_0
\]

其中,
\(W_0\)
表示初始权重,
\(W_i^{t=0}\)
表示第
\(t\)
代的第
\(i\)
个个体。

评估


本例中,采用让AI对弈的方式,根据AI在棋局中的表现评估AI得分,具体流程如下:

  1. 生成一个从1到N的随机排列,并将其按顺序分配给AI
  2. 将序号为1、3、5、...的AI与序号为2、4、6、...的AI对弈
  3. 将棋局结果记录到AI得分表内。
  4. 是否完成
    \(N_R\)
    轮对局,倘若未完成,则返回到1。
  5. 对AI进行排名。

交叉


当完成排名时,让排名后50%的AI及前50%的AI两两组合,其数学公式如下

\[\begin{align*}
W_{i}^{t+1}&=W_i^t\times(1-c)+W_{i-50}^t\times c, &N/2 \leq &t \leq N \\
W_{i}^{t+1}&=W_i^t\times(1-c)+W_{i+50}^t\times c, &0 \leq &t \leq N/2
\end{align*}
\]

其中:
\(c\)
为学习因子(交叉率),表示AI在学习过程中对新知识(权重)的接受程度,
\(c\)
越大,AI越倾向于接受新权重,
\(c\)
越小,AI越倾向于保留旧权重。交叉率
\(c\)
一搬可取
\(0.01\sim0.3\)

替换


首先定义局部最优个体和全局最优个体。

  • 局部最优
    \(W_b^t\)
    :如果一个个体在本轮中的综合成绩排名为第一名(胜场最多),那么称其为局部最优个体。

  • 全局最优
    \(W_B\)
    :当只进行一轮迭代时,全局最优个体等于局部最优个体,即:
    \(W_B=W_b^{t=0}\)
    。当进行了不止一局游戏时,将新的局部最优个体与全局最优个体进行
    \(N_R\)
    轮对局,倘若全局最优个体获胜,则其依旧为全局最优个体,倘若其失败,则局部最优个体成为新的全局最优个体。可以用数学公式表示为:

\[W_B=
\begin{cases}
W_b^t &\text{if}\;W_b^t \;\text{win},\\
W_B &\text{otherwise}.
\end{cases}
\]

为了保留最优的性状,将排名靠后的部分个体替换为全局最优个体,记替换率为
\(s\)
,一般取
\(0.02\sim 0.1\)

变异


在变异过程中,个体的基因发生随机的改变。定义变异系数
\(m\)
,其绝对了变异的程度,一般来说
\(m\)
的范围在
\(0.01\sim0.1\)
数学公式如下:

\[W_{i,j}^{t}=W^t_{i,j}\times (1+m_j)
\]

其中
\(W_{i,j}^{t}\)
表示第
\(t\)
代的第
\(i\)
个个体的第
\(j\)
个权重,
\(m_j\)
是在
\((-m,m)\)
内的随机数。

流程汇总


以下给出遗传算法学习的流程

  1. 初始化种群

  2. 创建棋局,各个个体互相对战,统计得分并进行排名

  3. 判断是否达到停止条件,若不是则继续。

  4. 依排名将个体两两匹配,进行交叉操作

  5. 将排名靠后的个体分别替换为局部最优个体和全局最优个体

  6. 进行变异操作

  7. 转至步骤2

附录

行为优先级

  • Lv1:下子直接取胜,或在一回合内取胜。
  • Lv2:下在大概率在若干回合内取胜。
  • Lv3:能够迫使对方一直防御。
  • Lv4:收益较低。

初始权重表

综合价值排序 己方价值 敌方价值 对应的奖励数值
1 Lv1 ? \(2^{20}\)
2 ? Lv1 \(2^{16}\)
3.1 Lv2 Lv2 \(2^{13}\)
3.2 Lv2 Lv3 \(2^{12}\)
3.3 Lv2 Lv4 \(2^{11}\)
4.1 Lv3 Lv2 \(2^{9}\)
4.2 Lv4 Lv2 \(2^{8}\)
5.1 Lv3 Lv3 \(2^{6}\)
5.2 Lv3 Lv4 \(2^{4}\)
6.1 Lv4 Lv3 \(2^{2}\)
6.2 Lv4 Lv4 \(2^{0}\)

符号说明

符号 意义 数值范围
\(W\) 个体(权重) -
\(R\) 行动的奖励 -
\(K\) 对劣选项的接受程度 -
\(N\) 种群大小 10~100
\(N_R\) 评估时的对局轮数 10~100
\(T\) 迭代次数 20~500
\(c\) 交叉率 0.01~0.03
\(s\) 替换率 0.02~0.1
\(m\) 变异率 0.01~0.1

LeetCode516 .最长回文子序列

题目叙述:

力扣题目链接(opens new window)

给你一个字符串
s
,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

  • 1 <= s.length <= 1000
  • s
    仅由小写英文字母组成

动态规划思路

  • 我们在上文中已经介绍了
    回文子串
    ,那么我们可以沿用
    回文子串
    的思想解决这道题,但是我们首先得明确
    回文子串

    回文子序列
    的区别

  • LeetCode647.回文子串
    求的是回文子串,而本题要求的是回文子序列, 要搞清楚这两者之间的区别。

  • 回文子串是要连续的,回文子序列可不是连续的!
    回文子串,回文子序列都是动态规划经典题目。

  • 回文子串,可以做这两题:


    • 647.回文子串

    • 5.最长回文子串

思路其实是差不多的,但本题要比求回文子串简单一点,因为情况少了一点。

动规五部曲分析如下:

1.确定状态变量及其含义

  • 我们设立dp数组,dp[i]] [j] 表示s字符串在
    [i,j]
    范围内最长回文子序列的长度。(
    j
    >=
    i
  • 那么我们确立了状态变量
    dp[i][j]
    ,那么我们就要开始处理递推公式和如何初始化了

2.确定递推公式

  • 在这里,我们最重要的就是判断
    s[i],s[j]
    之间的关系
    • s[i]==s[j]
      此时,
      dp[i][j]=dp[i+1][j-1]+2
  • 为什么是+2呢?因为本题是最长回文子序列,当
    s[i]==s[j]
    时,
    [i,j]
    范围内至少有
    dp[i+1][j-1]+2
    这个大小的最长回文子序列,+2就是加上
    s[i],s[j]
    这两个字符。

516.最长回文子序列

  • 如果
    s[i]与s[j]不相同
    ,说明
    s[i]和s[j]
    的同时加入 并不能增加
    [i,j]
    区间回文子序列的长度,那么分别加入
    s[i]、s[j]
    看看哪一个可以组成最长的回文子序列。

加入
s[j]
的回文子序列长度为
dp[i + 1] [j]

加入
s[i]
的回文子序列长度为
dp[i] [j - 1]

那么
dp [i] [j]
一定是取最大的,即:
dp [i] [j] = max(dp [i + 1] [j], dp[i] [j - 1])
;

516.最长回文子序列1

3.如何初始化dp数组

  • 首先,我们得处理特殊情况,当
    i==j
    的时候,这个时候在
    [i,j]
    范围内只有一个字符,使用
    dp[i][j]=dp[i+1][j-1]+2
    会导致当前处理的子串的左边界大于右边界,此时我们就得特殊处理一下,当处理的子串只有一个字符时,
    i==j
    ,并且
    dp[i][j]
    显然等于1,因为单个字符也是回文子序列,并且这个回文子序列的长度是1。
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;

4. 确定遍历顺序

从递归公式中,可以看出,
dp[i][j]
依赖于
dp[i + 1][j - 1]

dp[i + 1][j] 和 dp[i][j - 1]
,如图:

img

  • 所以说我们想要得到
    dp[i][j]
    ,必须从左下方开始,向着右上方的方向进行递推。
  • 所以说遍历顺序就是从下到上,从左到右
        //开始对dp数组进行从下到上,从左到右进行赋值。
        for(int i=s.size()-1;i>=0;i--){
            for(int j=i+1;j<s.size();j++){
                if(s[i]==s[j]) dp[i][j]=dp[i+1][j-1]+2;
                else dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
            }
        }

5.举例打印dp数组

输入s:"cbbd" 为例,dp数组状态如图:

516.最长回文子序列3

红色框即:
dp[0][s.size() - 1];
为最终结果。

最终代码:

//最长回文子序列
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        //创建二维的dp数组
        vector<vector<int>> dp(s.size(),vector<int>(s.size(),0));
        //初始化dp数组,首先要将i和j相等的时候,也就是只有一个字符的子序列,它的dp值赋值为1
        for(int i=0;i<s.size();i++) dp[i][i]=1;
        //开始对dp数组进行从下到上,从左到右进行赋值。
        for(int i=s.size()-1;i>=0;i--){
            for(int j=i+1;j<s.size();j++){
                if(s[i]==s[j]) dp[i][j]=dp[i+1][j-1]+2;
                else dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
            }
        }
        //最后,从0-s.size()-1这个范围的最长回文子序列的长度就是我们需要的答案。
        return dp[0][s.size()-1];
    }
};

注明

  • 本文中引用了作者
    代码随想录
    的部分图片和原文,若想深入了解,可以去原作者的文章阅读
  • 代码随想录

之前写过
两篇关于Roslyn源生成器生成源代码的用例
,今天使用Roslyn的代码修复器
CodeFixProvider
实现一个cs文件头部注释的功能,

代码修复器会同时涉及到
CodeFixProvider

DiagnosticAnalyzer
,

实现FileHeaderAnalyzer

首先我们知道修复器的先决条件是分析器,比如这里,如果要对代码添加头部注释,那么分析器必须要给出对应的分析提醒:

我们首先实现实现名为
FileHeaderAnalyzer
的分析器:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class FileHeaderAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "GEN050";
    private static readonly LocalizableString Title = "文件缺少头部信息";
    private static readonly LocalizableString MessageFormat = "文件缺少头部信息";
    private static readonly LocalizableString Description = "每个文件应包含头部信息.";
    private const string Category = "Document";

    private static readonly DiagnosticDescriptor Rule = new(
        DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

    public override void Initialize(AnalysisContext context)
    {
        if (context is null)
            return;

        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSyntaxTreeAction(AnalyzeSyntaxTree);
    }

    private static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context)
    {
        var root = context.Tree.GetRoot(context.CancellationToken);
        var firstToken = root.GetFirstToken();

        // 检查文件是否以注释开头
        var hasHeaderComment = firstToken.LeadingTrivia.Any(trivia => trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia));

        if (!hasHeaderComment)
        {
            var diagnostic = Diagnostic.Create(Rule, Location.Create(context.Tree, TextSpan.FromBounds(0, 0)));
            context.ReportDiagnostic(diagnostic);
        }
    }
}

FileHeaderAnalyzer分析器的原理很简单,需要重载几个方法,重点是
Initialize
方法,这里的
RegisterSyntaxTreeAction
即核心代码,
SyntaxTreeAnalysisContext
对象取到当前源代码的
SyntaxNode
根节点,然后判断TA的第一个
SyntaxToken
是否为注释行(SyntaxKind.SingleLineCommentTrivia|SyntaxKind.MultiLineCommentTrivia)

如果不为注释行,那么就通知分析器!

实现了上面的代码我们看一下效果:

image

并且编译的时候分析器将会在错误面板中显示警告清单:

image

实现CodeFixProvider

分析器完成了,现在我们就来实现名为
AddFileHeaderCodeFixProvider
的修复器,

/// <summary>
/// 自动给文件添加头部注释
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddFileHeaderCodeFixProvider))]
[Shared]
public class AddFileHeaderCodeFixProvider : CodeFixProvider
{
    private const string Title = "添加文件头部信息";
    //约定模板文件的名称
    private const string ConfigFileName = "Biwen.AutoClassGen.Comment";
    private const string VarPrefix = "$";//变量前缀
    //如果模板不存在的时候的默认注释文本
    private const string DefaultComment = """
        // Licensed to the {Product} under one or more agreements.
        // The {Product} licenses this file to you under the MIT license.
        // See the LICENSE file in the project root for more information.
        """;

    #region regex

    private const RegexOptions ROptions = RegexOptions.Compiled | RegexOptions.Singleline;
    private static readonly Regex VersionRegex = new(@"<Version>(.*?)</Version>", ROptions);
    private static readonly Regex CopyrightRegex = new(@"<Copyright>(.*?)</Copyright>", ROptions);
    private static readonly Regex CompanyRegex = new(@"<Company>(.*?)</Company>", ROptions);
    private static readonly Regex DescriptionRegex = new(@"<Description>(.*?)</Description>", ROptions);
    private static readonly Regex AuthorsRegex = new(@"<Authors>(.*?)</Authors>", ROptions);
    private static readonly Regex ProductRegex = new(@"<Product>(.*?)</Product>", ROptions);
    private static readonly Regex TargetFrameworkRegex = new(@"<TargetFramework>(.*?)</TargetFramework>", ROptions);
    private static readonly Regex TargetFrameworksRegex = new(@"<TargetFrameworks>(.*?)</TargetFrameworks>", ROptions);
    private static readonly Regex ImportRegex = new(@"<Import Project=""(.*?)""", ROptions);

    #endregion

    public sealed override ImmutableArray<string> FixableDiagnosticIds
    {
        //重写FixableDiagnosticIds,返回分析器的报告Id,表示当前修复器能修复的对应Id
        get { return [FileHeaderAnalyzer.DiagnosticId]; }
    }

    public sealed override FixAllProvider GetFixAllProvider()
    {
        return WellKnownFixAllProviders.BatchFixer;
    }

    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var diagnostic = context.Diagnostics[0];
        var diagnosticSpan = diagnostic.Location.SourceSpan;

        context.RegisterCodeFix(
            CodeAction.Create(
                title: Title,
                createChangedDocument: c => FixDocumentAsync(context.Document, diagnosticSpan, c),
                equivalenceKey: Title),
            diagnostic);

        return Task.CompletedTask;
    }


    private static async Task<Document> FixDocumentAsync(Document document, TextSpan span, CancellationToken ct)
    {
        var root = await document.GetSyntaxRootAsync(ct).ConfigureAwait(false);

        //从项目配置中获取文件头部信息
        var projFilePath = document.Project.FilePath ?? "C:\\test.csproj";//单元测试时没有文件路径,因此使用默认路径

        var projectDirectory = Path.GetDirectoryName(projFilePath);
        var configFilePath = Path.Combine(projectDirectory, ConfigFileName);

        var comment = DefaultComment;

        string? copyright = "MIT";
        string? author = Environment.UserName;
        string? company = string.Empty;
        string? description = string.Empty;
        string? title = document.Project.Name;
        string? version = document.Project.Version.ToString();
        string? product = document.Project.AssemblyName;
        string? file = Path.GetFileName(document.FilePath);
        string? targetFramework = string.Empty;
#pragma warning disable CA1305 // 指定 IFormatProvider
        string? date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
#pragma warning restore CA1305 // 指定 IFormatProvider


        if (File.Exists(configFilePath))
        {
            comment = File.ReadAllText(configFilePath, System.Text.Encoding.UTF8);
        }

        #region 查找程序集元数据

        // 加载项目文件:
        var text = File.ReadAllText(projFilePath, System.Text.Encoding.UTF8);
        // 载入Import的文件,例如 : <Import Project="..\Version.props" />
        // 使用正则表达式匹配Project:
        var importMatchs = ImportRegex.Matches(text);
        foreach (Match importMatch in importMatchs)
        {
            var importFile = Path.Combine(projectDirectory, importMatch.Groups[1].Value);
            if (File.Exists(importFile))
            {
                text += File.ReadAllText(importFile);
            }
        }

        //存在变量引用的情况,需要解析
        string RawVal(string old, string @default)
        {
            if (old == null)
                return @default;

            //当取得的版本号为变量引用:$(Version)的时候,需要再次解析
            if (version.StartsWith(VarPrefix, StringComparison.Ordinal))
            {
                var varName = old.Substring(2, old.Length - 3);
                var varMatch = new Regex($@"<{varName}>(.*?)</{varName}>", RegexOptions.Singleline).Match(text);
                if (varMatch.Success)
                {
                    return varMatch.Groups[1].Value;
                }
                //未找到变量引用,返回默
                return @default;
            }
            return old;
        }

        var versionMatch = VersionRegex.Match(text);
        var copyrightMath = CopyrightRegex.Match(text);
        var companyMatch = CompanyRegex.Match(text);
        var descriptionMatch = DescriptionRegex.Match(text);
        var authorsMatch = AuthorsRegex.Match(text);
        var productMatch = ProductRegex.Match(text);
        var targetFrameworkMatch = TargetFrameworkRegex.Match(text);
        var targetFrameworksMatch = TargetFrameworksRegex.Match(text);

        if (versionMatch.Success)
        {
            version = RawVal(versionMatch.Groups[1].Value, version);
        }
        if (copyrightMath.Success)
        {
            copyright = RawVal(copyrightMath.Groups[1].Value, copyright);
        }
        if (companyMatch.Success)
        {
            company = RawVal(companyMatch.Groups[1].Value, company);
        }
        if (descriptionMatch.Success)
        {
            description = RawVal(descriptionMatch.Groups[1].Value, description);
        }
        if (authorsMatch.Success)
        {
            author = RawVal(authorsMatch.Groups[1].Value, author);
        }
        if (productMatch.Success)
        {
            product = RawVal(productMatch.Groups[1].Value, product);
        }
        if (targetFrameworkMatch.Success)
        {
            targetFramework = RawVal(targetFrameworkMatch.Groups[1].Value, targetFramework);
        }
        if (targetFrameworksMatch.Success)
        {
            targetFramework = RawVal(targetFrameworksMatch.Groups[1].Value, targetFramework);
        }

        #endregion

        //使用正则表达式替换
        comment = Regex.Replace(comment, @"\{(?<key>[^}]+)\}", m =>
        {
            var key = m.Groups["key"].Value;
            return key switch
            {
                "Product" => product,
                "Title" => title,
                "Version" => version,
                "Date" => date,
                "Author" => author,
                "Company" => company,
                "Copyright" => copyright,
                "File" => file,
                "Description" => description,
                "TargetFramework" => targetFramework,
                _ => m.Value,
            };
        }, RegexOptions.Singleline);

        var headerComment = SyntaxFactory.Comment(comment + Environment.NewLine);
        var newRoot = root?.WithLeadingTrivia(headerComment);
        if (newRoot == null)
        {
            return document;
        }
        var newDocument = document.WithSyntaxRoot(newRoot);

        return newDocument;
    }
}

代码修复器最重要的重载方法
RegisterCodeFixesAsync
,对象
CodeFixContext
包含项目和源代码以及对应分析器的信息:

比如:
CodeFixContext.Document
表示对应的源代码,
CodeFixContext.Document.Project
表示对应项目,
CodeFixContext.Document.Project.FilePath
就是代码中我需要的
*.csproj
的项目文件,

我们取到项目文件,那么我们就可以读取配置在项目文件中的信息,比如
Company
,
Authors
,
Description
,甚至上一篇我们提到的版本号等有用信息,当前我用的正则表达式,当然如果可以你也可以使用
XPath
,
然后取到的有用数据替换模板即可得到想要的注释代码片段了!

比如我的Comment模板文件
Biwen.AutoClassGen.Comment

// Licensed to the {Product} under one or more agreements.
// The {Product} licenses this file to you under the MIT license. 
// See the LICENSE file in the project root for more information.
// {Product} Author: {Author} Github: https://github.com/vipwan
// {Description}
// Modify Date: {Date} {File}

替换后将会生成如下的代码:

// Licensed to the Biwen.QuickApi under one or more agreements.
// The Biwen.QuickApi licenses this file to you under the MIT license. 
// See the LICENSE file in the project root for more information.
// Biwen.QuickApi Author: 万雅虎 Github: https://github.com/vipwan
// Biwen.QuickApi ,NET9+ MinimalApi CQRS
// Modify Date: 2024-09-07 15:22:42 Verb.cs

最后使用
SyntaxFactory.Comment(comment)
方法生成一个注释的
SyntaxTrivia
并附加到当前的根语法树上,最后返回这个新的
Document
即可!

大功告成,我们来看效果:
image

以上代码就完成了整个代码修复器步骤,最后你可以使用我发布的nuget包体验:

dotnet add package Biwen.AutoClassGen

源代码我发布到了GitHub,欢迎star!
https://github.com/vipwan/Biwen.AutoClassGen

https://github.com/vipwan/Biwen.AutoClassGen/blob/master/Biwen.AutoClassGen.Gen/CodeFixProviders/AddFileHeaderCodeFixProvider.cs