2024年10月

前言

快速将创意变为现实!无需实体建库建表即可完成增删改查操作,支持15种条件查询、分页、列表及无限级树形列表等多种功能的API部署。

提供完善的接口文档、Auth授权、接口限流、客户端真实IP获取、先进服务器缓存组件及动态API等特性。让大家的工作效率倍增,远离加班和额外的知识付费。

项目介绍

  • 无需实体数据库,即可进行数据的增删改查

  • 支持15种条件查询

  • 提供分页、列表、无限级树形列表等功能

  • 提供详细的接口文档

  • 包含Auth授权机制

  • 支持接口限流和获取客户端真实IP

  • 拥有先进的服务器缓存组件

  • 支持动态API

  • 快速部署API

项目特点

为了让非技术人员也能轻松使用,我们特别发布了适用于 Linux、Mac 和 Windows 平台的 x64 和 x32 版本的应用程序,以及各平台的二进制文件。只需下载并直接启动即可运行。

启动项目后,在浏览器中输入
http://你的IP:3000/index.html
即可访问管理系统。

本系统无需安装任何额外环境即可启动运行,但数据库等外部软件需自行安装。

可以通过修改软件配置文件夹
Configuration
中的设置来调整系统行为:

  • Database.config
    文件用于配置数据库,默认使用 SQLite;

  • App.json
    文件包含软件的相关配置,其中
    urls
    字段允许您自定义软件的启动端口。”

项目依赖

  • 动态 API 解决方案:Panda.DynamicWebApi
  • 高性能 ORM 框架:SqlSugar
  • 自动生成 Swagger 接口文档:Swashbuckle.AspNetCore
  • 支持跨平台(Linux、macOS、Windows),无需安装额外环境,直接运行
  • SoybeanAdmin:基于最新前端技术栈(Vue3、Vite5、TypeScript、Pinia 和 UnoCSS)
  • FastCrud(简称 fs):面向配置的 CRUD 开发框架,基于 Vue3,助力快速开发 CRUD 功能,适合作为低代码平台的基础框架

项目环境

1、服务端启动

  • 使用 Visual Studio 2022 或 JetBrains Rider 打开
    SuperApi.sln
  • 确保已安装 .NET 8 SDK。

  • SuperApi
    设置为启动项目并运行,即可启动服务端。

2、前端项目启动

  • 使用 VSCode 打开
    admin-ui
    目录。
  • 在命令行中执行
    pnpm install
    以安装依赖。
  • 运行
    pnpm run dev
    启动前端项目。
  • 这样组织后,每个步骤的重点更加突出,用户可以更容易地跟随指导进行操作。

项目使用

后台配置

1、打开
SuperApi/SuperApi.sln
解决方案,进入
Configuration
目录,配置数据库及其他设置。

2、将
SuperApi
设为启动项目后直接运行。

前端页面

1、打开
admin-ui
文件夹,在命令行中执行
pnpm install
来安装依赖(如未安装 pnpm,请先执行
npm install -g pnpm
)。

2、安装完成后,执行
pnpm run dev
启动开发服务器。

3、启动后,浏览器将自动打开接口文档页面,您可以开始使用了。

登录信息

账号:admin/sp123456

项目效果

1、登录页

2、系统首页

3、订单管理

4、接口文档

项目地址

Gitee:
https://gitee.com/tmm-top/SuperApi

总结

本文只展示了部分功能和内容,如有需求访问项目地址获取详细信息。希望本文能在.NET开发方面为各位提供有益的帮助。期待大家在评论区留言交流,分享您的宝贵经验和建议。

最后

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

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

使用doccano标注NER数据详细教程

说明:

部署doccano

https://github.com/doccano/doccano
有说明如何部署。比如使用Docker部署:

docker run --name doccano \
  -d --restart always \
  -e "ADMIN_USERNAME=admin" \
  -e "ADMIN_EMAIL=admin@example.com" \
  -e "ADMIN_PASSWORD=password" \
  -v doccano-db:/data \
  -p 8001:8000 doccano/doccano

创建用户

默认只有一个用户,我们需要打开ADMIN管理页面添加新的用户。

在主网址后加
/admin/
然后打开ADMIN管理页面(注意后边的斜杠是必须的),点击
Add

添加用户名和密码信息后,点击
SAVE
以保存:

如何进行NER标注

创建项目

默认的界面是英语的,不习惯英语的话,可以切换为中文:

然后点击登录,输入用户名和密码登录,登录之后:

点击
创建
,会跳转到以下页面:

点击以选择
序列标注
(Sequence Labeling),然后输入名称等必要信息,根据需要配置其他属性:

点击
创建
,跳转到以下页面:

导入数据集

单击左侧的
数据集
按钮:

移动鼠标到
操作
按钮:

点击导入数据集:

doccano
支持多种格式的文本,它们的区别如下:

  • Textfile
    :上传的文件为
    txt
    格式,打标时一整个
    txt
    文件显示为一页内容;
  • Textline
    :上传的文件为
    txt
    格式,打标时
    txt
    文件的一行文字显示为一页内容;
  • JSONL

    JSON Lines
    的简写,每行是一个有效的
    JSON
    值;
  • CoNLL

    CoNLL
    格式的文件,每行均带有一系列制表符分隔的单词;

上传一个TXT文件:

点击导入后:

定义标签

点击左侧的
标签
,然后移动鼠标到
操作
菜单后点击
创建标签

创建3个常见的标签,
PER
,
LOC
,
ORG
,实际应用中需要根据需求确定有哪些标签。以下以创建
PER
标签为例:

创建完后:

添加成员

点击左侧的
成员
按钮,然后点击
增加

选择需要添加到项目的用户和角色,其中有3种角色(项目管理员 ,标注员,审查员)。选择好后保存:

保存后可以看到:

分配标注任务

首先,选中需要分配的数据:

然后,点击操作菜单下的
Assign to member

选择分配方案,然后点击右侧的
Assign
按钮

以上分配方案将15%的任务分配给
admin
用户,85%的任务分配给
user1
用户。

查看分配结果:

标注

点击左侧
数据集
,然后选择一条数据,点击最右边的
标注
按钮开始标注。

举例来说,点击右侧的
PER
标签,然后鼠标分别选择文本中的对应文字:

标注完成后,点击文本左上角的X按钮表示已标注完成:

导出数据

点击左侧
数据集
按钮,移动鼠标到
操作
菜单,点击
导出数据集

选择
JSONL
格式,勾选
Export only approved documents
(仅导出已审核过的数据),然后点击导出:

前言

在mongo中数据类型有很多种,常见的包括:

数据类型 例子 描述
String { "x" : "foot" } 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer { "x" : 1 } 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Object { "x" : { "y" : "foot" } } 用于内嵌文档
Array { "x" : [ "a" , "b" ] } 用于将数组或列表或多个值存储为一个键。

有一种很常见的查询,就是过滤数组中的一些数据,只返回符合要求的数据。数据如下,将下面travel中的vehicle=train的记录保留,过滤掉其他的元素,并返回整个文档。

{
    "name": "tom", 
    "travel": [
        {
            "vehicle" : "train",
            "city" : "北京"
        },
        {
            "vehicle" : "plane",
            "city" : "上海"
        }, 
        {
            "vehicle" : "train",
            "city" : "深圳"
        }
    ]
}

想要实现数组的过滤有三种方法,包括:

  1. 聚合查询 使用
    $unwind

    travel
    数组打散,获取结果集后用
    $match
    筛选符合条件的数据,最后使用
    $group
    进行聚合获取最终结果集
  2. 聚合查询 使用
    $match
    过滤符合条件的根文档结果集,然后使用
    $projec
    t返回对应字段的同时,在
    travel
    数组中使用
    $filter
    进行内部过滤,返回最终结果集
  3. 普通查询 先筛选记录,然后通过投影查询过滤数组

下面来分析这三种方法能否实现需求。

添加数据

假设有两条记录,每条记录是一个人的信息,包括姓名、职业、旅游过的城市。旅游过的城市是一个数组,包含城市的名字以及交通工具。

db.test.insertOne({
    "uid" : "1000001",
    "name" : "zhangsan",
    "job": "coder",
    "travel" : [ 
        {
            "vehicle" : "train",
            "city" : "北京"
        }, 
        {
            "vehicle" : "plane",
            "city" : "上海"
        }, 
        {
            "vehicle" : "train",
            "city" : "深圳"
        }
    ]
})
db.test.insertOne({

    "uid" : "1000002",
    "name" : "lisi",
    "job": "coder",
    "travel" : [ 
        {
            "vehicle" : "plane",
            "city" : "北京"
        }, 
        {
            "vehicle" : "car",
            "city" : "上海"
        }, 
        {
            "vehicle" : "train",
            "city" : "深圳"
        }
    ]
})
db.test.find()
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: 
   [ { vehicle: 'train', city: '北京' },
     { vehicle: 'plane', city: '上海' },
     { vehicle: 'train', city: '深圳' } ] }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: 
   [ { vehicle: 'plane', city: '北京' },
     { vehicle: 'car', city: '上海' },
     { vehicle: 'train', city: '深圳' } ] }

验证三种方法

需求说明

现在的目标是:筛选的出所有记录中通过火车去旅游的城市,也就是travel数组中vehicle=train的记录,过滤掉非目标记录。

方法一

方法一:使用
$unwind

travel
数组打散,获取结果集后用
match
筛选符合条件的数据,最后使用
$group
进行聚合获取最终结果集。

db.getCollection('test').aggregate(
    [
        {   
            $unwind: "$travel" 
        },
        { 
            $match : {
                "job":"coder", 
                "travel.vehicle": "train" 
            } 
        },
        { 
            $group : { 
                "_id" : "$uid", 
                "travel": { $push: "$travel" } 
            } 
        } 
    ]
)

结果:

{ _id: '1000002', travel: [ { vehicle: 'train', city: '深圳' } ] }
{ _id: '1000001', travel: [ { vehicle: 'train', city: '北京' }, { vehicle: 'train', city: '深圳' } ] }

分析:

unwind 可以将一个数组拆分,例如unwind的效果如下:

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '北京' } }
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'plane', city: '上海' } }
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'plane', city: '北京' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'car', city: '上海' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }

然后通过match筛选出符合条件的数据

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '北京' } }
{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  job: 'coder',
  travel: { vehicle: 'train', city: '深圳' } }

最后通过group进行聚合,以_id为聚合依赖,合并相同_id的数据。

总结:

这种方法是能够达到过滤数组的要求,但是有一个问题,拆分数组比较简单,想要再合并起来就不容易了。group只能以某一个变量为基准聚合,其他变量都会丢失。比如最后的结果只保留了_id和travel,其他变量都丢失了。

方法二

方法二:使用
$match
过滤符合条件的根文档结果集,然后使用
$project
返回对应字段的同时,在
travel
数组中使用
$filter
进行内部过滤,返回最终结果集

db.getCollection('test').aggregate(
    [
        { 
            $match : { "job": "coder" } 
        },
        {
            $project: {
                "uid": 1,
                "name": 1,
                "travel": {
                    $filter: {
                        input: "$travel",
                        as: "item",
                        cond: { $eq : ["$$item.vehicle","train"] }
                    }
                }
            }
        }
    ]
)

结果分析:

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  travel: [ { vehicle: 'train', city: '北京' },{ vehicle: 'train', city: '深圳' } ] }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  travel: [ { vehicle: 'train', city: '深圳' } ] }

分析:

mongo中查询分为两种:普通查询和高级查询。高级查询包括聚合查询,用aggregate关键字实现。

MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。

这里我们介绍一下聚合框架中常用的几个操作:

  • $project
    :修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
  • $match
    :用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。
  • $limit
    :用来限制MongoDB聚合管道返回的文档数。
  • $skip
    :在聚合管道中跳过指定数量的文档,并返回余下的文档。
  • $unwind
    :将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
  • $group
    :将集合中的文档分组,可用于统计结果。
  • $sort
    :将输入文档排序后输出。
  • $geoNear
    :输出接近某一地理位置的有序文档。

这里首先使用match过滤所有job=coder,然后使用project修改输出的结构。在project中使用了filter来过滤数组中的元素。

filter的定义如下:

根据指定条件选择要返回的数组的子集。返回仅包含与条件匹配的那些元素的数组。返回的元素按原始顺序。

$filter
具有以下语法:

{ $filter: { input: <array>, as: <string>, cond: <expression> } }
领域 规格
input 解析为数组的
表达式
as 可选的。代表数组中每个单独元素的
变量
名称
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">input</font></u>
。如果未指定名称,则变量名称默认为
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">this</font></u>
cond
表达式
可解析为布尔值,该布尔值用于确定输出数组中是否应包含元素。该表达式
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">input</font></u>
使用在中指定的变量名称分别引用数组的每个元素
<u><font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">as</font></u>

https://mongodb.net.cn/manual/reference/operator/aggregation/filter/

在cond将vehicle=train的元素留下,排除其他元素。

总结:

这种方法可以完成查询目标,既可以过滤掉数组中的元素,也可以返回完整的文档。

方法三

方法三:

通过投影查询,先选择符合条件的记录,在通过使用投影操作符,需要返回的字段,以及排除特定的字段。

db.test.find(
      {
         job: "coder"
      }, 
      {  
          uid: 1, 
          name: 1, 
          travel: {
             $filter: {
                input: "$travel",
                as: "item",
                cond: { $eq : ["$$item.vehicle","train"] }
             } 
          } 
      }
)

结果:

{ _id: ObjectId("6708d3e646d2075ca11e88ce"),
  uid: '1000001',
  name: 'zhangsan',
  travel: 
   [ { vehicle: 'train', city: '北京' },
     { vehicle: 'train', city: '深圳' } ] }
{ _id: ObjectId("6708d3f646d2075ca11e88cf"),
  uid: '1000002',
  name: 'lisi',
  travel: [ { vehicle: 'train', city: '深圳' } ] }

分析:

什么是投影查询?

在MongoDB中,投影查询是一种查询操作,用于选择性地返回文档中的字段。通过使用投影操作符,我们可以指定需要返回的字段,以及是否要排除特定的字段。

投影查询语法如下所示:

db.collection.find({ <query> }, { <projection> })

其中,
是一个查询表达式,用于筛选满足条件的文档。 是一个可选参数,用于指定要返回的字段。

在projection中保留字段、排除字段、
选择或排除数组中的特定元素。利用选择或排除数组中的特定元素的特性也可以达到目的。

例如:

如果我们只想返回每个文档中的第一个标签,我们可以这样做:

db.products.find({}, { tags: { $slice: 1 } })

在本篇中通过filter方法来过滤数组,保留符合条件的元素。

总结:

该方法能够完成查询目标,并且是一种简洁的实现,普通查询复杂度低,而且没有太多关键字的使用。

参考文档

https://geek-docs.com/mongodb/mongodb-questions/393_mongodb_mongo_query_with_projection.html

https://segmentfault.com/a/1190000016629733

https://mongodb.net.cn/manual/reference/operator/aggregation/filter/

https://blog.csdn.net/weixin_44009447/article/details/115479348

近年来,随着前端技术的飞速发展,各类后台管理系统框架层出不穷。
Vue
作为热门的前端框架,也有许多优秀的后台模板涌现。而
Vue-Vben-Admin
,凭借其高效、灵活的架构设计和完善的功能体系,成为了许多前端开发者的不二选择。其
Github Star达
到了
24K
之多,可见其受欢迎程度。本文将详细介绍 Vue-Vben-Admin 的特点、使用方法以及适用场景,帮助大家深入了解这个强大的工具。

简要介绍

Vue-Vben-Admin
是基于
Vue3

Vite

TypeScript
构建的后台管理系统模板。它集成了
Vue3
生态中的多种先进技术,如
Pinia
作为状态管理工具、
Vue Router
作为路由管理工具,并通过
Shadcn UI
提供了一套现代化的 UI 组件,具有强大的扩展性和模块化特性。无论是开发中小型项目,还是大型企业级应用,
Vue-Vben-Admin
都能够提供一个强大、灵活、可扩展的管理后台框架。

显著特性

  • 最新技术栈
    :使用 Vue3/vite 等前端前沿技术开发
  • TypeScript
    : 应用程序级 JavaScript 的语言
  • 主题
    :提供多套主题色彩,可配置自定义主题
  • 国际化
    :内置完善的国际化方案
  • 权限
    :内置完善的动态路由权限生成方案

使用方式

  1. 安装依赖
git clone https://github.com/vbenjs/vue-vben-admin.git
cd vue-vben-admin
pnpm install
  1. 运行
pnpm serve

启动成功后,你可以在浏览器中打开
http://localhost:3000
,访问后台管理系统。
3. 自定义配置
项目配置文件位于
src/config
目录中,开发者可以根据项目需求自定义
API 地址

权限设置
以及
主题风格

适用场景

Vue-Vben-Admin
的高扩展性和模块化设计,使得它能够适应多种场景的后台管理系统开发:

  1. 企业级后台管理系统
    Vue-Vben-Admin
    内置的
    权限管理

    模块化架构
    非常适合用于开发大型企业级管理系统,支持复杂的
    用户角色

    权限控制
    ,满足企业的各种业务需求。

  2. 中小型项目管理平台
    对于中小型项目,
    Vue-Vben-Admin
    提供了快速构建的能力,开箱即用的功能和高效的开发工具,使开发者能够快速搭建功能完善的管理平台。

  3. SaaS 平台
    Vue-Vben-Admin
    具有高度的灵活性,能够根据不同的
    SaaS
    平台需求进行功能扩展和定制,同时支持多用户权限体系的灵活配置,是开发
    SaaS
    平台管理系统的理想选择。

  4. 技术学习与练手项目
    作为一个集成了
    Vue3
    生态中各种先进技术的项目,
    Vue-Vben-Admin
    非常适合用于学习和实践前端技术的开发者,通过它可以深入学习
    Vue3

    Vite

    TypeScript

    Pinia
    等技术的使用与应用。

结语

Vue-Vben-Admin
作为一个基于
Vue3
的后台管理模板,凭借其高效的构建工具、完善的功能体系、灵活的模块设计,为前端开发者提供了强大的支持。不论是大型企业的管理系统,还是个人项目的后台框架,它都能帮助开发者快速搭建出一个高性能、高扩展性的后台系统。如果你正在寻找一个功能强大、易于扩展的
Vue3
管理系统模板,不妨试试
Vue-Vben-Admin
,它将是你开发项目的好帮手。


该模版已经收录到我的全栈前端一站式开发平台
“前端视界”
中(浏览器搜
前端视界
第一个),感兴趣的欢迎查看!

今天我们将开始第二个数据类型-链表的学习,同样我们还是用最原始的方式,自己申请内存管理内存来实现一个链表。

01
、01、定义

什么是链表?链表在物理存储结构上表现为非顺序性和非连续性,因此链表的数据元素物理存储位置是随机的,动态分配的;而在逻辑结构上表现为线性结构的特点,即元素一个连着一个元素串起来像一条线 。

节点
:其中链表元素又叫节点,一个节点主要包含数据域和指针域,其中数据域主要存放数据元素,而指针域主要存放下一个节点存储位置地址。

头指针
:一个表示链表第一个节点位置的普通指针,并且永远指向第一个节点位置,方便后面使用链表。

头节点
:通常表示链表的第一个节点,并且节点内数据域为空,因此也叫空节点,其作用主要用于解决一些特殊问题,因此也可以省略。

首元节点
:由于头节点数据域为空,因此链表的第一个数据域不为空的节点叫首元节点,只是一个名称,并没有什么实际意义。

02
、02、分类

链表有两种分类方法,其一可以分为静态链表和动态链表,其二可以分为单向链表、双向链表以及循环链表。

单链表只有一个方向,每个节点包含数据域和指向下一个节点的指针域。

双向链表有两个方向,即每个节点包含数据域以及同时指向上一个节点和下一个节点的指针域。

循环链表指链表首尾相连,即最后一个节点的指针域指向第一个节点。循环链表也分单向循环链表和双向循环链表,原理都一样。

03
、03、实现

下面我们一起使用最原始的方式,自己申请内存空间,自己维护,完成链表的实现。

1、ADT定义

我们首先来定义链表的ADT(单链表)。

ADT LinkedList{

数据对象:D 是一个非空的元素集合,D = {a1, a2, ..., an},其中 ai 表示一个元素即节点,一个节点存储着数据和指向下一个节点的指针。

数据关系:D中的节点通过指针进行连接,每一个节点都包含一个指向下一个节点的指针。

基本操作:[

Init(n) :初始化一个空链表,即声明一个头指针,如有必要也可以声明一个头节点。

Length:返回链表长度。

HeadNode:返回头节点。

Find(v):返回数据域v对应的节点。

Update(n,v):更新n节点的数据域。

InsertAfter(n,v):在n节点后面添加数据域为v的新节点。

Remove(n):移除n节点。

Destroy():销毁链表。

]

}

定义好链表ADT,下面我们就可以开始自己实现一个数据域为string类型的链表。

2、定义类

首先我们需要定义节点,其中包含两个字段一个是存放数据、一个是存放指针,代码如下。

public struct MyselfLinkedListNode
{
    //数据域
    public string Data { get; set; }
    //指针域
    public IntPtr Next { get; set; }
}

然后再定义链表实现类MyselfLinkedList,用来实现链表的相关操作。

因为我们直接管理内存,所以需要一个维护内存的指针字段;

因为我们直接获取链表长度,所以需要一个存储链表长度字段;

因此我们的MyselfLinkedList类初步是这样的:

public sealed class MyselfLinkedList : IDisposable
{
    //申请内存起始位置指针
    private IntPtr _head;
    //链表长度
    private int _length;
}

3、初始化Init

初始化结构主要做几件事。

a.分配内存空间;

b.什么头指针;

c.创建头节点;

d.维护链表长度属性;

具体实现代码如下:

//初始化链表,声明头指针,并创建头节点
public MyselfLinkedListNode Init()
{
    //计算节点的大小
    var size = Marshal.SizeOf(typeof(MyselfLinkedListNode));
    //分配指定字节数的内存空间
    _head = Marshal.AllocHGlobal(size);
    //创建头节点
    var node = new MyselfLinkedListNode
    {
        Data = null,
        Next = IntPtr.Zero
    };
    //将节点实例写入分配的内存
    Marshal.StructureToPtr(node, _head, false);
    //链表长度加1
    _length++;
    //返回头节点
    return node;
}

4、获取链表长度 Length

这个比较简单直接把链表长度私有字段返回即可。

//链表长度
public int Length
{
    get
    {
        return _length;
    }
}

5、获取头节点 HeadNode

获取头节点主要是为了方便数据处理,可以通过头指针直接读取内存地址获取。具体代码如下:

//头节点
public MyselfLinkedListNode? HeadNode
{
    get
    {
        if (_head == IntPtr.Zero)
        {
            return null;
        }
        return GetNode(_head);
    }
}
//获取节点
private MyselfLinkedListNode GetNode(IntPtr pointer)
{
    // 从分配的内存读取实例
    return Marshal.PtrToStructure<MyselfLinkedListNode>(pointer);
}

同样我们也可以定义一个尾节点属性,可以方便使用,原理都差不多,这里就不赘述了。

6、在指定节点后插入节点 InsertAfter

通过前面对链表结构的了解,要想再两个节点之间加入一个新节点,只需要把两者之间的线剪断,即前一个节点的指针域需要重新指向新节点,并且新节点的指针域要指向后一个节点,其他保持不变,如下图:

业务逻辑清楚了,我们再来梳理代码逻辑,要想实现这个功能我们大致需要一下几步:

a.获取指定节点的指针;

b.创建一个新的节点;

c.重新调整指定节点及新节点指针域;

d.把指定节点和新节点指针调整后数据更新到内存中;

e.更新链表长度属性;

具体实现如下:

//在指定节点后插入新节点
public MyselfLinkedListNode InsertAfter(MyselfLinkedListNode node, string value)
{
    //获指定取节点对应指针
    var pointer = GetPointer(node);
    //如果指针不为空才处理
    if (pointer != IntPtr.Zero)
    {
        //以新值创建一个节点
        var (newPointer, newNode) = CreateNode(value);
        //把新节点的下一个节点指针指向指定节点的下一个节点
        newNode.Next = node.Next;
        //把指定节点的下一个节点指针指向新节点
        node.Next = newPointer;
        //更新修改后的节点
        Marshal.StructureToPtr(newNode, newPointer, false);
        Marshal.StructureToPtr(node, pointer, false);
        //链表长度加1
        _length++;
        return newNode;
    }
    return default;
}
//获取节点对应指针
private IntPtr GetPointer(MyselfLinkedListNode node)
{
    //从头指针开始查找
    var currentPointer = _head;
    //如果当前指针为空则停止查找
    while (currentPointer != IntPtr.Zero)
    {
        //获取当前指针对应的节点
        var currentNode = GetNode(currentPointer);
        //如果当前节点数据域和指针域与要查找的节点相同则返回当前节点指针
        if (currentNode.Data == node.Data && currentNode.Next == node.Next)
        {
            return currentPointer;
        }
        //否则查找下一个节点
        currentPointer = currentNode.Next;
    }
    return IntPtr.Zero;
}
//创建节点
private (IntPtr Pointer, MyselfLinkedListNode Node) CreateNode(string value)
{
    //计算大小
    var size = Marshal.SizeOf(typeof(MyselfLinkedListNode));
    //分配指定字节数的内存空间
    var pointer = Marshal.AllocHGlobal(size);
    //创建实例并设置值
    var node = new MyselfLinkedListNode
    {
        Data = value,
        Next = IntPtr.Zero
    };
    //将实例写入分配的内存
    Marshal.StructureToPtr(node, pointer, false);
    //返回节点指针和节点
    return (pointer, node);
}

这里只实现了一个在指定节点后插入节点,我们还可以实现在指定节点前插入,在首元节点前插入,在尾节点后添加,都是可以的,感兴趣的可以自己实现试试。

7、根据数据域查找节点 Find

在链表中对查找是不友好的,因为查找一个值,需要从链表头一个一个往后查找,实现逻辑到不复杂,具体实现代码如下:

//根据数据查找节点
public MyselfLinkedListNode Find(string value)
{
    //从头指针开始查找
    var pointer = _head;
    //如果当前指针为空则停止查找
    while (pointer != IntPtr.Zero)
    {
        //获取当前指针对应的节点
        var node = GetNode(pointer);
        //如果当前节点数据域和要查找值相同则返回当前节点
        if (node.Data == value)
        {
            return node;
        }
        //否则查找下一个节点
        pointer = node.Next;
    }
    return default;
}

8、更新指定节点数据域 Update

这个方法逻辑也比较简单,只需要找到节点指针,然后把节点更新,最后把更新后的数据写入内存即可。

//更新节点数据
public void Update(MyselfLinkedListNode node, string value)
{
    //获取节点对应指针
    var pointer = GetPointer(node);
    //当指针不为空,则更新节点数据
    if (pointer != IntPtr.Zero)
    {
        //修改数据
        node.Data = value;
        //将数据写入分配的内存,完成数据更新
        Marshal.StructureToPtr(node, pointer, false);
    }
}

9、移除指定节点 Remove

如果要想移除一个节点,则需要把指定节点与前后节点的连接删除,然后把前后两个节点建立起连接,同时需要手动释放被删除节点内存。如下图。

具体代码实现如下:

//移除节点
public void Remove(MyselfLinkedListNode node)
{
    //从头指针开始查找
    var currentPointer = _head;
    //获取当前节点
    var currentNode = GetNode(_head);
    //查找节点对应的指针
    var pointer = GetPointer(node);
    while (true)
    {
        if (currentNode.Next == IntPtr.Zero)
        {
            //指针为空则返回
            return;
        }
        else if (currentNode.Next == pointer)
        {
            //把要删除节点的上一个节点对应的下一个节点指向要删除节点的下一个节点
            currentNode.Next = node.Next;
            //手动释放被删除节点对应的内存
            Marshal.FreeHGlobal(pointer);
            //更新要删除节点的上一个节点
            Marshal.StructureToPtr(currentNode, currentPointer, false);
            //链表长度减1
            _length--;
            break;
        }
        else
        {
            //查找下一个节点
            currentPointer = currentNode.Next;
            currentNode = GetNode(currentPointer);
        }
    }
}

10、销毁链表 Destroy

销毁链表主要是使用因为是我们自己手动管理内存,用完后要及时清理,放在内存泄漏等意外情况出现。代码也很简单,循环把每个节点内存释放即可,如下代码:

//销毁链表
public void Destroy()
{
    var pointer = _head;
    while (pointer != IntPtr.Zero)
    {
        var value = GetNode(pointer);
        Marshal.FreeHGlobal(pointer);
        _length--;
        pointer = value.Next;
    }
    _head = IntPtr.Zero;
}

11、释放内存 Dispose

因为我们实现了IDisposable接口,所有需要实现Dispose方法,只需要在Dispose方法中调用上面销毁链表Destroy方法即可。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner