首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2026年6月7日, 农历四月廿二, 星期日
2
「今日早报」 2026年6月6日, 农历四月廿一, 星期六
3
「今日早报」 2026年5月16日, 农历三月卅十, 星期六
4
「今日早报」 2026年5月15日, 农历三月廿九, 星期五
5
「今日早报」 2026年5月14日, 农历三月廿八, 星期四
沙漠渔
把過去的累積,善用到當下
累计撰写
2,789
篇文章
累计创建
385
个标签
累计收到
997
条评论
栏目
首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
搜索
标签搜索
代理服务
winsw
override
VMware
api
popai
拉取镜像
人工智能
copilot
chatgpt
openai
coze
objdump
ldd
日志
版本
latest
批处理
bat
节能模式
iwconfig
排序
du
设计模式
hostname
面板
cockpit
版本不兼容
npm
统计
烟花
新春
leveldb
Java heap space
堆内存
harbor
utf8mb4
网络聚合
IPV6
nmtui
测速
带宽
千兆
路由器
nmcli
nmlci
orangepi
motd
中文乱码
webdav
香橙派
代码折叠
享元模式
单例模式
解锁
锁定
无法安装
Xshell
并发编程
ScheduledThreadPoolExecutor
ThreadPoolExecutor
线程池
Fock-Join
并发
ExecutorService
nextcloud
alist
panic
hung task
时间戳
ping
tail
dd
嵌入式
点灯
mount
共享
NFS
curl
全屏
ChannelOption
C++
comparator
桥头堡
开源
varchar
char
StringBuilder
StringBuffer
String
命令行工具
网络配置
netsh
建议
专家
离谱
2022
设备管理器
虚拟网卡
环回适配器
安全
攻击
CC
DDOS
跳槽
过年
年假
春节
2023
优站计划
鼻塞
咳嗽
大号流感
阳
新冠病毒
循环冗余校验
CRC-16/XMODEM
crc校验
stream
DMZ主机
域名解析
CDN
七牛云
DDNS
类加载器
双亲委派
加载机制
删除
搜索
变化时间
修改时间
访问时间
响应异常
超时
jsoup
用法详解
压缩命令
打包压缩
zip
优缺点
打包
数据压缩
密码
sudoers
索引量
权重
瞬间
魔鬼洞
沙漠鱼
沙漠渔
面试
引用传递
值传递
即时编译器
机器码
JIT
旗舰版
ISO
原版镜像
win7
参数
配置文件
8小时
时间错误
报告
coverity
jetbrains
idea
谷歌翻译
rest
403
ERR_UNKNOWN_URL_SCHEME
DevTools
优先级
location
FTP
挂载
curlftpfs
提示
自定义参数
springboot
配置
死锁
超级密码
桥接
联通光猫
Dockerfile
构建
命令行
eclipse
光猫
路由模式
桥接模式
bash-4.2
sudo
oracle帐号
JDK下载
百度收录
定时发布
哨兵模式
内存管理
JVM
登录
免密
不一致
服务器时间
BIO,NIO,AIO
ByteBuffer
FileWriter
BufferedOutputStream
流水线
pipeline
环境变量
重启
任务消失
jenkins
卸载
snap
20.04
Ubuntu
容器
大数据传输
InputStream
学习笔记
CSS
最佳实践
DOM操作
原型链
原型
No such file or Directory
SSH配置
SSH免密
tar
命令
网站优化
取消快照
百度快照
全局配置
修改密码
控制台
gitlab
解码器
Netty
文件传输
sz
rz
Jar
打铁花
羊毛沟
容器日志
docker
规范
博客
rejected
Gerrit
wireshark
代理
Go
http-server
nodejs
package-lock.json
996加班
删除标签
钟薛高
60s新闻接口
每日新闻接口
百度福利
自动回复
localstorage
QQ机器人
redis
近乎
客户端
JuiceSSH
乔恩
加菲猫
新浪邮箱
joe
公告
父亲节
图标
每日新闻
51la
服务器
腾讯云
插件
罗永浩
编码
avatar
小虎墩大英雄
端午劫
端午节
中石化
中国石化
儿童节
win10
激活
shell
金门大桥
旧金山
产品经理
免疫力
熬夜
云旅游
云游
招聘
海尔
halo
校友会
创新
哈工大
鸿蒙
王成录
标题
主题
北京
疫情
域名
沙漠渔溏
方法声明
jpa
表情包
emoji
JavaAgent
姓名测试
姓名打分
起名
百度
收录
SEO
group by
distinct
去重
JDBC
validationQuery
兄弟元素
点击事件
$event
9600
传输延时
波特率
串口
站点统计
站长工具
cnzz
生命周期
refs
vue
replaceAll
replace
JavaScript
软件
FRP
内网外入
内网穿透
学习
解题
leetcode
云计算
中国医药
神思电子
股票
Java
报错
数据块
关键词
风筝
清明节
注解
spring
clang-format
格式化
反向代理
nginx
索引
linux
vim
数据库
仓库管理
git
压缩
winrar
mysql
测试
markdown
目 录
CONTENT
以下是
网络聚合
相关的文章
2024-01-19
自己动手从零写桌面操作系统GrapeOS系列教程——10.NASM汇编语言
学习操作系统原理最好的方法是自己写一个简单的操作系统。 汇编语法主要有两种:Intel语法和AT&T语法。 由于大部分介绍x86汇编的书籍和资料用的都是Intel语法,毕竟x86就是Intel发明的,大家学过的x86汇编大概率也是Intel语法,所以GrapeOS的汇编也用Intel语法。 支持Intel语法的x86汇编器不止一种,常见的有MASM和NASM。MASM来自微软公司,只能在微软的操作系统Windows和MS-DOS上用。NASM是开源软件,支持多平台。GrapeOS选用NASM。 下面介绍一下NASM在Linux上的使用 1.首次使用需要先安装 yum install nasm 由于我之前已经安装过了,所以在上面的截图上显示“无须任何处理”。本教程中后续用到的Linux命令如果提示没有,一般都通过 yum install命令安装即可。 2.使用NASM汇编器 我们以第5讲中的代码为例: nasm boot.asm -o boot.bin nasm 命令后面紧跟的boot.asm是汇编代码的源文件,参数-o后面跟的是生成文件的文件名。 从上面的截图我们可以看到生成了boot.bin文件。这里的boot.bin是一个二进制文件,里面就是生成的机器码。CPU只认识机器码。把这个文件中的内容给CPU,CPU就能执行。 下面我们补充一下如何查看二进制文件。 对于文本文件,Linux下可以使用cat命令查看,比如我们用cat命令查看boot.asm文件: 如果我们用cat查看二进制文件boot.bin: 从上面的截图中可以看到,用cat查看二进制文件会显示乱码。 在Linux中可以使用hexdump命令来查看二进制文件。 hexdump boot.bin 我们来看上面的截图,hexdump命令默认显示的是十六进制数。左边第一列是每一行数据在文件内的起始字节索引,右边其它列都是文件内的数据。文件内的数据每两个字节写在一起,高地址字节写在前面,低地址字节写在后面,一行共显示16个字节。在第5行下面一行只有一个星号“*”,表示相同的行重复若干行,直到遇到不同的行为止。星号上面一行都是0,这里星号表示重复了若干行0,直到最后一行数据不是全0。在最后一行数据下面一行有个数字,表示文件总字节数,这里是0x200,换算成十进制数是512。 在实际使用时可以加上一个参数-C: hexdump boot.bin -C 效果如下图: 从上面的截图可以更加清晰的看到文件中每个字节的十六进制数。此时右边多了一大列,用竖线包裹者的数据,里面很多是点号。其实这是同一行中的16个字节分别以ASCII字符显示的结果,ASCII中的不可显示字符在这里就都用点号代表了。我们用hexdump命令查看一个文本文件大家对比一下就明白了,下面以汇编源文件boot.asm文件为例。 hexdump boot.asm -C 从上面截图上可以看到,中间区域是文件中每个字节的十六进制数,右边竖线列中显示的就是每个字节对应的ASCII字符。 本讲对应的视频版地址:https://www.bilibili.com/video/BV1PT411X7VG/ GrapeOS操作系统交流QQ群:643474045
2024-01-19
458
0
0
网络聚合
2024-01-19
【论文笔记】UNet
语义分割的U-Net网络结构Unet是2015年诞生的模型,它几乎是当前segmentation项目中应用最广的模型。Unet能从更少的训练图像中进行学习,当它在少于40张图的生物医学数据集上训练时,IOU值仍能达到92%。Unet网络非常简单,前半部分作用是特征提取,后半部分是上采样。在一些文献中也把这样的结构叫做编码器-解码器结构。由于此网络整体结构类似于大写的英文字母U,故得名U-net。 论文链接: https://arxiv.org/pdf/1505.04597v1.pdf github: https://github.com/milesial/Pytorch-UNet 1 Motivation 生物医学图像处理面临的问题 经典卷积网络大部分都是针对图像分类任务的,但是在一些特定场景,如医疗图像处理领域,应是pixel-wise像素级的处理,输入输出均是图像,即图像分割。 生物医学任务中没有很多标注的数据集 为了解决这两个问题,Ciresan用滑窗法来预测patch的类别(patch指像素周围的局部区域)。 该算法有两个主要问题:(1)由于每个patch都需要训练导致这个算法很慢,且patch之间有很多重复。(2)定位准确率和上下文联系之间需要平衡,patch越大需要pooling越多准确率越低,patch越小则不具备上下文联系。 2 U-Net网络 作者以FCN全卷积神经网络为基础设计了Unet,其中包含两条串联的路径:contracting path用来提取图像特征,捕捉context,将图像压缩为由特征组成的feature maps;expanding path用来精准定位,将提取的特征解码为与原始图像尺寸一样的分割后的预测图像。 和FCN相比,U-Net的第一个特点是完全对称,也就是左边和右边是很类似的,而FCN的decoder相对简单,只用了一个deconvolution的操作,之后并没有跟上卷积结构。第二个区别就是skip connection,FCN用的是加操作(sum),U-Net用的是叠操作(concat)。最重要的是编码和解码(encoder-decoder)的思路,编码和解码常用于压缩图像和去噪声,后来这个思路被用在了图像分割上,非常简洁好用。 网络左边一侧作者称之为contracting path,右边一侧为expanding path。 蓝色箭头为卷积层,卷积层的stride=1,padding=0,因此卷积后特征层的宽高会减2。卷积层后接ReLU激活函数,没有BN层(BN由Google于2015年提出)。 池化层stride=2,池化后宽高减半,通道数不变。池化层之后的卷积层将通道数翻倍。 绿色的up-conv是转置卷积,将特征层的宽高×2,通道数减半。 灰色copy and crop是先对左边的特征层进行中心裁剪(保留中心特征),再与右边path对应的特征层进行通道数上的concat。 最后的1×1的卷积没有ReLU,输出通道数为类别数。 Overlap-tile 可以发现Unet论文中输入的图像是572×572,但是输出图像大小为388×388。也就是说推理上图黄色部分,需要蓝色区域内的图像数据作为输入。当黄色区域位于边缘时,就会产生边缘数据缺失的情况(上图右边蓝框中的空白部分)。我们可以在预处理中,对输入图像进行padding,通过padding扩大输入图像的尺寸,使得最后输出的结果正好是原始图像的尺寸,同时输入图像块(黄框)的边界也获得了上下文信息从而提高预测的精度,本文用的是mirror padding。我们自己搭建网络的时候,输入输出往往是一样大小的(padding=1),因此不需要考虑这个问题。 3 训练 3.1 数据增强 网络需要大量标注训练样本,生物医学任务中没有数千个标注的数据集,所以需要对数据进行数据扩张。作者采用了弹性变形的图像增广,以此让网络学习更稳定的图像特征。因为数据集是细胞组织的图像,细胞组织的边界每时每刻都会发生不规则的畸变,所以这种弹性变形的增广是非常有效的。论文笔记:图像数据增强之弹性形变(Elastic Distortions) 3.2 损失函数的权重 细胞组织图像的一大特点是,多个同类的细胞会紧紧贴合在一起,其中只有细胞壁或膜组织分割。因此,作者在计算损失的过程中,给两个细胞的边缘部分及细胞间的背景部分增加了损失的权重,以此让网络更加注重这类重合的边缘信息。 如上图所示,图(a)为原始图像,图(b)为人工标注的实例分割ground truth,图(c)为mask,图(d)为每个像素的损失权重weight map。首先用形态学操作获得边界,再用下面的公式计算weight map 其中,wc是为了类别平衡,d1是该像素到最近细胞边界的距离,d2是到第二近的细胞边界的距离。在作者实验中设置w0=10,σ≈5pixels. 3.3 其他 优化器:SGD + momentum(0.99) batch:为了最大限度的使用GPU显存,比起输入一个大的batch size,更倾向于大量输入tiles,因此实验batch size为1。 损失函数:pixel-wise softmax + cross_entropy 初始化高斯分布权重:在具有许多卷积层和通过网络的不同路径的深度网络中,权重的良好初始化非常重要。 否则,网络的某些部分可能会进行过多的激活,而其他部分则永远不会起作用。 理想情况下,应调整初始权重,以使网络中的每个特征图都具有大约单位方差。作者用的高斯分布的权重。 参考 1. 精读论文U-Net 2. 论文笔记:图像数据增强之弹性形变(Elastic Distortions) 3. 研习U-Net
2024-01-19
273
0
0
网络聚合
2024-01-19
浅谈 C++ 模板 & 泛化 (妈妈再也不用担心我不会用 std::sort 了)
基础复习 先上个对 int 类型数组的插入排序: void insertionSort_01(int* seq, int firstIndex, int lastIndex) { for (int j = firstIndex + 1; j <= lastIndex; ++j) { int key = seq[j]; int i = j - 1; while (i >= firstIndex && key < seq[i]) { seq[i + 1] = seq[i]; --i; } seq[i + 1] = key; } } 提出问题: 如果想排 double 类型数组怎么办? 可以重载一个 double 版本: void insertionSort_01_b(double* seq, int firstIndex, int lastIndex) { ... } 当然, 更好的方式是利用 C++ 的模板泛化元素类型: template<class ElemType> void insertionSort_02(ElemType* seq, int firstIndex, int lastIndex) { ... } 步入正题 接着提出两个问题: 1 是否一定要求升序排列 2 ElemType 对象是否一定能使用 operator< 为解决问题 1, 我们可以额外写个降序排列版本: template<class ElemType> void insertionSort_02_b(ElemType* seq, int firstIndex, int lastIndex) { for (...) { ... // Change {<} to {>} when comparing {key} and {seq[i]}: while (i >= firstIndex && key > seq[i]) { ... } ... } } 对于问题 2, 我们举个例子. 现有: struct MyStruct { int aa; int bb; }; MyStruct arr_MyStruct[4] = { {1,4},{3,1},{9,-1},{12,0} }; 要求对 arr_MyStruct 中的元素以 MyStruct::aa 排序. 对于 C++ 新手来说, 这是一个比较难解决的问题, 也是问题 2 聚焦的关键. 对问题 1 的处理中, 我们将 "比较" 这个谓语 (predicate) 从 operator< 替换为 opeartor>; 这给了我们一些提示: 是否可以像我们用模板来泛化元素类型那样泛化谓语? 提出概念: 函数对象 (function object) 定义类 bad_greater: // Omit the definition of class <MyStruct>. struct bad_greater { // {operator()} should be defined as a const method, // in order to make it available to <const bad_greater> instances. bool operator()(const MyStruct& left, const MyStruct& right) const { return left.aa > right.aa; } }; 称 bad_greater 所创建的实例为函数对象, 可以参考以下使用案例: // Omit the definition of class <MyStruct>. MyStruct arr_MyStruct[4] = { {1,4},{3,1},{9,-1},{12,0} }; bad_greater compare; std::cout << compare(arr_MyStruct[0], arr_MyStruct[1]) << std::endl; // Use anonymous instance: std::cout << bad_greater()(arr_MyStruct[0], arr_MyStruct[1]) << std::endl; bad_greater 之所以 bad, 是因为其唯独提供对类 MyStruct 实例的比较. 定义一个模板类 good_less 并对 MyStruct 偏特化以解决这个问题: // Omit the definition of class <MyStruct>. template<class T> struct good_less { bool operator()(const T& left, const T& right) const { return left < right; } }; template<> struct good_less<MyStruct> { bool operator()(const MyStruct& left, const MyStruct& right) const { return left.aa < right.aa; } }; 有了函数对象, 我们可以泛化算法中的谓语: template<class ElemType, class _Pred> void insertionSort_03(ElemType* seq, int firstIndex, int lastIndex, const _Pred& compare) { for (...) { ... while (... && compare(key, seq[i])) { ...; } ... } } 调用函数 insertionSort_03() 时, 我们要注意, 编译器能直接根据传入参数推断模板的实例化类型; 因此无需提供额外的模板类参数: // Omit the definition of class <MyStruct>. // Omit the definition of class <good_less>. // Omit the definition of class <good_greater>. MyStruct arr_MyStruct[4] = { {1,4},{3,1},{9,-1},{12,0} }; // Ascending order: insertionSort_03(arr_MyStruct, 0, 3, good_less<MyStruct>()); // Descending order: insertionSort_03(arr_MyStruct, 0, 3, good_greater<MyStruct>()); // Also works for array with orther types: double arr_double[4] = { 1,9.1,0.9,-3.1 }; insertionSort_03(arr_MyStruct, 0, 3, good_greater<double>()); std::sort() 的升降序排序 std::sort() 和我们的 insertionSort_03() 一样泛化的谓语, 而且 STL 还提供了类 std::greater 和 std::less 等用于定义函数对象. 升降序的使用方法参考以下代码: #include <algorithm> #include <functional> double arr_double[4] = { 1,9.1,0.9,-3.1 }; // Ascending order: std::sort(arr_double, arr_double + 4); // Ascending order: std::sort(arr_double, arr_double + 4, std::less<double>()); // Descending order: std::sort(arr_double, arr_double + 4, std::greater<double>())); 你可能会问: 为什么第一个例子不用和之前说的一样, 传入一个函数对象? 这没什么高深的, 在 C++14 之前, 其实只是额外提供了一个只有两个参数的函数重载而已. 给个差不多的伪代码出来: std::sort(seq_begin, seq_end){ std::sort(seq_begin, seq_end, std::less()); } C++14 之后在谓语类和 std::sort() 的定义上用了点小 trick, 下面给点启发性的例子 (如果不感兴趣, 你可以跳过这段): template<class T = void> struct less { template<class T> bool operator()(const T& a, const T& b) const { return a < b; } }; template<..., class _Pred = less<void>> void sort(..., const _Pred& compare = {}) { ... } 说简单点, 就是 less 给了一个默认模板实例化类型 void; 而真正进行比较的 operator() 又是一个模板. 调用 sort 时, 不用考虑第三个参数 (函数对像) 具体是什么类型, 反正 operator() 在比较时会自行实例化. 可以参考以下使用案例: // Under C++ 14 (or later) standard. #include <algorithm> #include <functional> double arr_double[4] = { 1,9.1,0.9,-3.1 }; std::sort(arr_double, arr_double + 4); // std::less<void> std::sort(arr_double, arr_double + 4, std::less()); // std::less<void> std::sort(arr_double, arr_double + 4, std::less<double>()); // std::less<double> int arr_int[4] = { 1,3,4,0 }; std::sort(arr_double, arr_double + 4, std::less()); // std::less<int> std::sort() 排其他类型实例 如果看懂了前面的内容, 想必你也能够猜出来怎么实现这个问题了. 注意, std::less 之类的谓语类型说到底就是结构体, 和我们上面实现的 good_less 没啥区别. 所以如果我们还是要排序上文提到的 MyStruct 数组: // Omit the definition of class <MyStruct>. // Omit the definition of class <good_less>. // Omit the definition of class <good_greater>. #include <algorithm> MyStruct arr_MyStruct[4] = { {1,4},{3,1},{9,-1},{12,0} }; // Ascending order: std::sort(arr_MyStruct, arr_MyStruct + 4, good_less<MyStruct>()); // Descending order: std::sort(arr_MyStruct, arr_MyStruct + 4, good_greater<MyStruct>()); 统一指针和迭代器 作为一个 STL 使用者, 难免会遇到指针与迭代器不统一的问题. 例如以下例子: // Use pointer: int arr_int[] = ...; std::sort(arr_int, ...); // Use iterator: std::vector<int> arr_vector = ...; std::sort(arr_vector.begin(), ...); 解决方式之一是统一泛化指针类型和迭代器类型, 这里把它们都当作类 _RandIt . 我们还是以最开始的 insertionSort 为例, 给出示范代码. 需要注意的是, 通过迭代器和指针获取元素类型 (用来定义 key )时, decltype 会保留解引用 (dereference) 后留下的引用 & (也就是说 decltype(arr_int[0]) 得到的类型不是 int 而是 int& ); 因此需要调用 std::remove_reference 来删除类型中的引用. using index = long long; template<class _RandIt, class _Pr = std::less<void>> void insertionSort(_RandIt seq, index firstIndex, index lastIndex, const _Pr& comp = {}) { for (index j = firstIndex + 1; j <= lastIndex; ++j) { typename std::remove_reference<decltype(*seq)>::type key = seq[j]; index i = j - 1; while (i >= firstIndex && comp(key, seq[i])) { seq[i + 1] = seq[i]; --i; } seq[i + 1] = key; } } 再给个归并排序的代码吧! 就说到这里 (计组完全没学, 寄). using index = long long; template<class _RandIt, class _Pr> void merge(_RandIt seq, index subAFirst, index subALast, index subBLast, auto MAX, auto MIN, const _Pr& comp) { auto END = comp(1, 2) ? MAX : MIN; size_t sizeSubA = subALast - subAFirst + 2; size_t sizeSubB = subBLast - subALast + 1; auto subA = new typename std::remove_reference<decltype(*seq)>::type[sizeSubA]; std::copy(seq + subAFirst, seq + subALast + 1, subA); subA[sizeSubA - 1] = END; auto subB = new typename std::remove_reference<decltype(*seq)>::type[sizeSubB]; std::copy(seq + subALast + 1, seq + subBLast + 1, subB); subB[sizeSubB - 1] = END; // Merge two subsequences to origin {seq[subAFirst : subBLast]}: for (index k = subAFirst, i = 0, j = 0; k <= subBLast; ++k) { if (i >= sizeSubA || j >= sizeSubB) return; // Merge: if (comp(subA[i], subB[j])) { seq[k] = subA[i]; ++i; } else { seq[k] = subB[j]; ++j; } } delete[] subA; delete[] subB; } template<class _RandIt, class _Pr = std::less<void>> void mergeSort(_RandIt seq, index firstIndex, index lastIndex, auto MAX, auto MIN, const _Pr& comp = {}) { if (firstIndex >= lastIndex) return; index mid = (firstIndex + lastIndex) / 2; mergeSort(seq, firstIndex, mid, MAX, MIN, comp); mergeSort(seq, mid + 1, lastIndex, MAX, MIN, comp); merge(seq, firstIndex, mid, lastIndex, MAX, MIN, comp); }
2024-01-19
197
0
0
网络聚合
2024-01-19
spring事务里面开启线程插入,报错了是否会回滚?
1.前言 一道非常有意思的面试题目。大概是这样子的,如果在一个事务中,开启线程进行插入更新等操作,如果报错了,事务是否会进行回滚 2.代码 示例1 @RequestMapping("/test/publish/submit") public String testPublish1() { log.info("start..."); transactionTemplate.execute(new TransactionCallback<String>() { @Override public String doInTransaction(TransactionStatus status) { TElement element = new TElement(); element.setfElementId(10L); element.setfElementName("111"); mapper.insertSelective(element); element = new TElement(); element.setfElementId(10L); element.setfElementName("222"); mapper.insertSelective(element); return "OK"; } }); log.info("end..."); return "ok"; } 示例2 @RequestMapping("/test/publish/submit2") public String testPublish2() { log.info("start..."); transactionTemplate.execute(new TransactionCallback<String>() { @Override public String doInTransaction(TransactionStatus status) { es.submit(() -> { TElement element = new TElement(); element.setfElementId(10L); element.setfElementName("111"); mapper.insertSelective(element); }); es.submit(() -> { TElement element = new TElement(); element.setfElementId(10L); element.setfElementName("222"); mapper.insertSelective(element); }); return "OK"; } }); log.info("end..."); return "ok"; } 3.结论 示例1 element.setfElementId(10L); 为主键。SQL在第一次插入id=10的时候是没有问题的,在第二次插入id=10的时候,由于主键冲突了,导致报错,然后整个事务都会进行回滚,这是没有问题的。是spring的事务帮助我们来进行回滚等操作的。我们可以看到如下代码,他是对整个result = action.doInTransaction(status);进行了try catch。如果抛异常,就会回滚 @Override @Nullable public <T> T execute(TransactionCallback<T> action) throws TransactionException { Assert.state(this.transactionManager != null, "No PlatformTransactionManager set"); if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) { return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action); } else { TransactionStatus status = this.transactionManager.getTransaction(this); T result; try { result = action.doInTransaction(status); } catch (RuntimeException | Error ex) { // Transactional code threw application exception -> rollback rollbackOnException(status, ex); throw ex; } catch (Throwable ex) { // Transactional code threw unexpected exception -> rollback rollbackOnException(status, ex); throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception"); } this.transactionManager.commit(status); return result; } } 示例2 示例2首先是transactionTemplate.execute是一个主main线程。然后在第一个子线程插入了一个数据,第二个子线程也插入了一个数据。那么现在就是有三个线程,一个是main线程,一个是A线程,一个是B线程。 main线程正常执行不报错,A线程正常插入不报错,B线程由于主键冲突报错。 我们可以通过上面action.doInTransaction(status);看出来,他对这块代码进行了try catch。也就是主线程进行了try catch。那么也就是只要主线程没有报错,这个事务就不会被捕获,也就不会回滚了。无论你A,B还是CDEFG子线程出问题了,只要不影响main线程,那事务就不会回滚呢? 因此我们可以得出一个结论,在示例2中,A线程会插入成功,B线程插入失败,事务不会回滚,最终插入成功。这个其实与我们平常的想法所违背了。 因此如果想要主线程抛出异常,得让主线程感知到子线程异常了,主动地去throw异常。比如我们可以设置一个flag,子线程报错了 flag=true。主线程检测到flag为true,就主动抛出一个exception 4.最后 这道面试题非常有意思,起初以为会回滚,没想到不会回滚。查看代码得知,原来是catch住的是主线程,并不是子线程。同样注解式事务类似。因此如果想要事务生效,尽量避免在事务中使用多线程来进行插入更新等操作
2024-01-19
217
0
0
网络聚合
2024-01-19
自己动手从零写桌面操作系统GrapeOS系列教程——18.外设和IO
学习操作系统原理最好的方法是自己写一个简单的操作系统。 一、外设和I/O接口 前面我们介绍过冯·诺依曼结构包含5部分,其中输入设备和输出设备统称为外部设备,简称外设。常见的外设有鼠标、键盘、显示器、硬盘等。由于外设种类多、差异大、速度慢等原因,导致CPU无法直接与外设沟通。于是在CPU和外设之间产生了“中间人”,这个“中间人”就是I/O接口。如下图: CPU与外设的信息交流都是通过I/O接口来间接实现的。比如我们前面向屏幕输出字符,并不是CPU直接将数据传递给屏幕,而是先传递给显卡,显卡再去操控屏幕。显卡就是一种I/O接口。后面我们将要学习如何读写硬盘,同样CPU无法直接读写硬盘,而是通过“中间人”硬盘控制器来间接实现读写硬盘。硬盘控制器也是一种I/O接口。上图中只举了显示器和硬盘两个例子,实际还有很多,图中用省略号代表了,后面我们学到哪个再讲哪个。 二、I/O端口和端口访问 我们知道在CPU内部有一些寄存器,而在每个I/O接口上面也都有一些寄存器,通常叫做I/O端口。CPU与I/O接口的交流,主要就是读写这些I/O端口,也叫端口访问。 CPU访问I/O端口有两种方式,一种是通过内存地址访问,另一种是通过端口号来访问。 1.通过内存地址访问端口 这种方式就是将一部分内存地址映射到相应的端口上,CPU读写这些端口就和读写内存的指令是一样的。 2.通过端口号访问端口 这种方式是为每一个端口分配一个唯一的编号,叫端口号,然后通过端口号就可以读写这个端口。我们后面主要用这种。 我们需要注意2点: 在x86架构中端口号的最大取值范围是0~65535,也就是2个字节表示的范围。 端口作为一种寄存器,它的数据宽度有的是8位,也有的是16位。具体是多少位,我们用到的端口都会介绍。 三、端口访问代码 1.读端口代码 读端口总共有4种方式,代码如下: in al,dx in ax,dx in al,立即数 in ax,立即数 以上4行代码,每一行都表示从指定端口读取数据到al或ax寄存器中。 这里需要注意2点: 源操作数就是端口号,只能用dx或立即数表示,而且立即数只能在0~255范围内。 目的操作数只能是al或ax,如果端口是8位的就用al,如果端口是16位的就用ax。 2.写端口代码 写端口也有4种方式,代码如下: out dx,al out dx,ax out 立即数,al out 立即数,ax 以上4行代码,每一行都表示将al或ax中的数据写入到指定的端口中。 同样需要注意2点: 目的操作数就是端口号,只能用dx或立即数表示,而且立即数只能在0~255范围内。 源操作数只能是al或ax,如果端口是8位的就用al,如果端口是16位的就用ax。 本讲视频版地址:https://www.bilibili.com/video/BV1ig4y1b7cs/ 配套的代码与资料在:https://gitee.com/jackchengyujia/grapeos-course GrapeOS操作系统交流QQ群:643474045
2024-01-19
221
0
0
网络聚合
2024-01-19
我希望来年,更多是靠关系和模式挣钱——2022年我的总结与思考
我记得是2017年开始在博客园写博客,那年我儿子出生,并与当年年底写了17年的年度总结。 n年前,我没钱但年轻,我怕n年后我老时,还是一无所成——2017我的收获和反思,在随后的几年的年末,写总结文也已经成为了我的惯例,后面几年的总结文链接如下。 2018我跳出了舒适区,发现自己缺的不仅是技术,另外还得探索其它挣钱渠道,这一年里,我从外企跳到了一家互联网公司,开始从技术角度真正去探索高并发分布式等值钱技术。 2019我获得的些许成绩,能不能弥补这一年光阴的流逝?盘点2019我的得失,当年我用从互联网公司学到的技术又跳回了外企。 我不想安于当前的限度,以达到所谓的幸福,回顾下2020年的我。当年我依然在外企,开始习惯于外企的工作和生活平衡。 今年我拿到了期望中的收入,同时更希望能在睡后收入上有进一步的发展——2021年我的总结与思考,21年,也就是去年,我继续在外企苟着。 由于我一直在博客园首发年度总结,今年凭借这份香火之情,继续在这里发表年终总结,当然在这之前,也敷衍性地把若干篇在其他地方发表的高赞文章搬到了此地,不管怎样,总是希望博客园越办越好吧。 1 平台比努力重要,今年虽然收益减少,但我不能再挑剔 22年的年初一,我还去了上海城隍庙,那时人头攒动,大摆长龙,但不料从3月中旬到6月初,就一直是在家办公,而临近23年,依然是在家办公,彼此考虑更多的是身体。 论今年的收益,虽然本职收益没变,但兼职收益比起去年有所降低,而且这个降低的幅度不能说是“略有降低”,但本人好歹每月能按时拿到薪资,这样的情况,本人应当满足,来年的期望,除了身体康健以外,首要的还是先期待工作稳定吧。 今年对大家来说,不能说是容易,本人周围的店铺关张的有不少,而听到的公司倒闭情况,比往年也略有增多,应届生的就业难度,比往年也有所提升。来年开春不知情况如何,不过本人还是想说,平台的选择要优于能力,对待我们做IT这行的人来说,还是尽可能地找些能多抗风险的大公司吧。 2 出齐了Spring Boot到Cloud的书,但讲课收益不得不被降低 今年本人出了本Spring Cloud Alibaba的书,Spring cloud alibaba是spring cloud的第二代组件。 算上之前本人出的Spring Cloud第一代组件的书,以及Spring Boot的书,本人在Spring Boot+Cloud的书算是能成一个系列。 之后本人打算出本Spring Boot+全栈带案例的书,这样也算是能在这方面继续探索吧。 不过出书本身不挣钱,而出书外加大公司背景加持外加讲课,这才能挣到钱。由于众所周知的原因,今年能做线上课的平台普遍都不好,而线下课也很难开起来,所以讲课方面的收益降了不少。 只是希望来年万事可期,尽早恢复,这样本人尚能以驽钝之才多多挣些外快,以求在大城市里继续苟且偷生。同时更祝各位不甘人下的朋友,也尽快早日实现财务自由。 3 在某平台的发文,能做到日点击过3w,但似乎利用率不高 在某乎,如果搜本人的用户,“老胡聊Java”,应当能看到本人的文章,主要讲java面试。在22年,本人在某平台发文,高光之时能做到日点击过3万,即到了中午,当天所有文章累积点击过1万,到了下班时间,当天总点击过2万,到了晚上10点,总体点击过3万。同时本人在该平台发的一些文章,能在1,2个月内累计10w点击,这也算是本人发文的回报吧。 不过即使有这样的点击,能给本人带来的直接收益并不多。如何利用这些点击,如何于当下众多平台通过发文发视频给自己带来更多的收益,这也是本人后面需要重点考虑的问题。 不过本人想说的是,成系列的产品,比如系列视频系列文,或者说是自建一个网站运营,这才能带来更多的收益,如果是零散发文,可能看似热闹,但真无法带来直接的收益,这也算是本人持续发文后的一些感受吧。 4 年入百万极难,但不得不作为努力方向 本人在去年制定的22年乃至之后的目标是,年入百万。虽然在某乎平台上似乎是人均百万,但要做到这个太难。 比如在大平台,哪怕是架构,每天忙个不停,连睡觉时手机也得放身边,这样累计升级到资深架构,年入能有80w真不错了。 再如自己开公司,看似风光,手下好几十号人,做的项目报价也好几十万甚至百万,但一方面每天可能得如履薄冰,战战兢兢地维护客户维护产品,另一方面项目还有风险,到年底一算,除去房租人力成本,剩下的也真未必多,如果能比打工多些,那也该谢天谢地了。 本人私下揣度,本人靠薪资,尚能以年入六七十万为目标,如果再要在此方面更进一步,似乎得多找几个朋友,一方面找些性价比比较高的活,另一方面可能得自己接项目接活干了。这要比打工难太多,但万事不进则退,有时候只能是明知不可为而为之。 5 想想来年,还是以“苟住”作为第一目标吧 做最好希望,做最坏的打算。虽然一方面得多找机会,但在一些形势下,创业或开实体店,确实可能属于逆势而为。 所以这里本人还是无奈地讲,来年首要的,还是先苟住吧,先确保工资收益,先确保家人和自身的身体,后面的情况,谁也预料不到,但总是先期待一切会变好吧。 或者再说句悲观的话,不管怎样,生活总是要继续,希望来年至少本人能少些“逆来顺受”吧。 6 期望春暖花开,期待万事好转 时代的尘埃,落在个人的身上,都是一座大山,但与其抱怨,还不如多做。可能当下做的努力收效甚微,但只能是这样说,多做总比放弃要好很多,毕竟做了就有希望,做了才能知道该努力的方向。 这张图是上海龙华寺的,从2017年的年末总结文到现在,我是一直用这张图为我自己,为我的家人和为大家祈福。 在22年的年底,再次用此图,祝福各位全家身体健康,诸事圆成,更希望所有人,包括我自己,能在新的一年里能心想事成。
2024-01-19
404
0
0
网络聚合
2024-01-19
如何管理项目干系人
一、什么是项目干系人? 项目干系人是指积极参与项目或其利益可能受项目积极或消极影响的个人、组织。我们以软件项目为例,大家也可以将项目干系人视为在软件项目中拥有既得利益的任何人,包括员工、客户、供应商、投资者、合作伙伴,甚至竞争对手。他们的目标和优先级可能会与团队不同。 二、什么是项目干系人管理? 我们可以这样定义项目干系人管理: 识别、参与并与项目干系人保持联系以确保项目总体成功的持续过程。通过管理或影响项目干系人,我们可以推动项目的成功。同时,对于企业战略来说,我们可以帮助企业识别出项目干系人,了解他们的需求和期望,在满足项目干系人的需求的基础上,实现企业的发展战略。 三、如何管理项目干系人? 1、想要更好地管理项目干系人,我们需要注意以下技巧 保持沟通畅通、有效:我们应该更及时、快速地了解到他们最新的需求; 明确要求:我们需要从项目一开始就明确项目干系人需要做什么、交付什么以及交付的截止日期; 建立关系:与项目干系人建立良好的关系是成功管理项目干系人的必要条件。我们应该更了解他们以及他们要关心的内容,并与他们建立一个融洽的关系; 及时反 馈:项目干系人所反馈的问题以及他们的担忧,我们应及时有效地进行处理。这反映出我们认真对待了他们的意见,并能及时对他们的需求做出回应。 2、管理项目干系人:决定我们想从项目干系人那里得到什么 首先我们需要考虑这些方面: 项目干系人在项目中的角色是什么? 项目干系人的影响力有多大? 项目干系人对项目的兴趣是什么? 项目干系人对项目成功起了多大的推动作用? 然后,让项目干系人在项目早期就参与进来,并定期与他们保持有效的沟通和联系,确保他们对项目的接受和支持。我们可以通过以下几种方式与项目干系人建立联系: 定期的会议同步; 发送产品版本更新及产品报告; 进行干系人调研。 3、管理项目干系人:频繁沟通 与项目干系人的沟通是项目管理的关键点。对于项目干系人来说,他们需要知道在整个项目过程中发生了什么,然后针对这些变化表达自己的意见或建议。 因此,频繁沟通能够帮助项目干系人与项目团队之间建立起信任和理解关系,同样也能够帮助他们参与到项目中,了解项目的重要性。我们可以通过几种不同的方式与项目干系人进行沟通:电子邮件、电话、与团队的定期会议以及项目管理协作工具等。 4、管理项目干系人:应公开透明 将目标和目的透明化:对于项目干系人来说,比较重要的一点是他们需要知道我们想要实现什么,以及为什么要这样做。当涉及到与他们的沟通时,要尽可能地解释清楚,这样能够加强双方之间的信任关系; 明确项目时间表:明确项目时间表及时间节点,能够让项目干系人明确知道他们什么时候可以得到交付成果; 明确项目风险:在管理风险时,我们应该提前告知可能会遇到的风险,并做好相应的预防措施; 明确 潜在挑战:当我们在项目过程中遇到了一些潜在的障碍或挑战时,应及时告知项目干系人,听听他们的想法。 5、管理项目干系人:应处事灵活 在管理项目干系人时,我们应灵活变通。项目干系人的需求会随着时间的推移而产生变化,这时我们应积极地拥抱变化,并根据需要调整自己的管理方式与处理方法。请记住以下小技巧: 乐于改变:根据项目干系人的反馈,不断地尝试新事物、调整计划; 对项目干系人的要求做出回应:如果项目干系人要求什么,尝试满足他们; 适当妥协:对于项目干系人的一些要求,我们可以尝试找到一个让双方都满意的中间地带。 项目干系人的管理是一门学问,总结来说就是以下三步: 对项目干系人进行调研、分析; 了解他们的需求和意愿、想法; 针对干系人的不同需求提出相应的解决措施。 干系人管理是一个持续不断的过程,也是项目管理中不可忽视的一部分,希望通过项目干系人管理,让大家能够“ 兵来将挡,水来土掩”!
2024-01-19
417
0
0
网络聚合
2024-01-19
游戏设计之-排行榜处理
前言 我相信大部分人,乃至公司和团队在设计排行榜都考虑的是redis,zadd操作,不需要排序,维护获取,操作都极其简单; 无一例外我也是; 在项目中运营了大量的模板,来处理各个木块的排行榜信息; 统一的会在晚上又一些结算处理;就牵涉一次性拉取,过滤,发放奖励,甚至还有删除操作; 同时为了节约运营陈本,定期还需要对redis里面无效数据进行删除,收缩操作; 目前redis已经达到6.55g,这只是其中一个平台运营部署; 运营过项目的都知道云redis的费用是非常高昂的;所以就需要定期做一些无用数据的删除操作; 可是问题来了,随着运营时间的推移,每天早上总能看到如此的异常推送; 就问你揪心不?仔细一查才发现还是数据库相关的操作导致的卡主了; 于是下定决心对排行榜等相关功能进行调整; 削峰 1. 首先就是对数据库的一些删除操作,集中早上5-6点去处理他;这个时候在线玩家最少;处理一些垃圾数据最为合理;基本卡了也不会对玩家的感觉特别强烈反馈; 于是把原来处理的数据移除的代码调整为定时器创建异步任务在凌晨5-6点去处理; 2. 第二步就是为了避免玩家集体拉取数据,并对数据进行操作;考虑重构排行榜代码需求, 排行榜自定义实现 为了模拟排行榜实现的, 首先应该考虑,排行榜数据,既方便查找修改,又方便排序; 那么肯定需要考虑的是 一个map,用于存取和玩家相关积分数据; 一个list,用于排序积分; 首先我们来实现一个积分类 1 package org.wxd.lang.rank; 2 3 import lombok.Getter; 4 import lombok.Setter; 5 import lombok.experimental.Accessors; 6 import org.wxd.io.ObjectFactory; 7 import org.wxd.timer.TimeUtil; 8 9 import java.util.Comparator; 10 import java.util.Objects; 11 12 /** 13 * 排行 14 * 15 * @author: Troy.Chen(無心道, 15388152619) 16 * @version: 2022-12-08 21:27 17 **/ 18 @Getter 19 @Setter 20 @Accessors(chain = true) 21 public class RankScore implements Comparable<RankScore> { 22 23 /** 正序 */ 24 public static final Comparator<RankScore> Sort = (o1, o2) -> { 25 if (o1.score != o2.score) { 26 return Double.compare(o1.score, o2.score); 27 } 28 29 if (o1.scoreTime != o2.scoreTime) { 30 /**时间取值要倒叙*/ 31 return Long.compare(o2.scoreTime, o1.scoreTime); 32 } 33 34 return Long.compare(o1.uid, o2.uid); 35 }; 36 37 /** 倒叙 */ 38 public static final Comparator<RankScore> BreSort = (o1, o2) -> { 39 if (o1.score != o2.score) { 40 return Double.compare(o1.score, o2.score); 41 } 42 43 if (o1.scoreTime != o2.scoreTime) { 44 /**时间取值要倒叙*/ 45 return Long.compare(o2.scoreTime, o1.scoreTime); 46 } 47 48 return Long.compare(o1.uid, o2.uid); 49 }; 50 51 private long uid; 52 private double score; 53 private long scoreTime; 54 55 public RankScore setScore(double score) { 56 this.score = score; 57 this.scoreTime = TimeUtil.currentTimeMillis(); 58 return this; 59 } 60 61 @Override public int compareTo(RankScore o2) { 62 return Sort.compare(this, o2); 63 } 64 65 public int scoreIntValue() { 66 return (int) score; 67 } 68 69 public long scoreLongValue() { 70 return (long) score; 71 } 72 73 @Override public boolean equals(Object o) { 74 if (this == o) return true; 75 if (o == null || getClass() != o.getClass()) return false; 76 RankScore rankScore = (RankScore) o; 77 return uid == rankScore.uid; 78 } 79 80 @Override public int hashCode() { 81 return Objects.hash(uid); 82 } 83 84 @Override public String toString() { 85 return ObjectFactory.stringBuilder(sb -> { 86 sb.append(this.getClass().getSimpleName()).append("{"); 87 sb.append("uid=").append(uid); 88 sb.append(", score=").append(score); 89 sb.append('}'); 90 }); 91 } 92 } View Code 接下来我实现代码 1 package test; 2 3 import org.junit.Test; 4 import org.wxd.lang.RandomUtils; 5 import org.wxd.lang.rank.RankScore; 6 import org.wxd.system.MarkTimer; 7 8 import java.util.ArrayList; 9 import java.util.HashMap; 10 import java.util.List; 11 12 /** 13 * @author: Troy.Chen(無心道, 15388152619) 14 * @version: 2022-12-08 20:48 15 **/ 16 public class RankPackTest { 17 18 public class RankPack { 19 HashMap<Long, RankScore> rankMap = new HashMap<>(); 20 List<Long> rankList = new ArrayList<>(); 21 22 public void addScore(long uid, double score) { 23 /*忽律并发问题 可以自行改为 ConcurrentHashMap*/ 24 rankMap.computeIfAbsent(uid, l -> { 25 RankScore rankScore = new RankScore().setUid(uid).setScore(score); 26 /*能到这里初始化,那么list里面必然也没有数据*/ 27 rankList.add(uid); 28 return rankScore; 29 }); 30 } 31 32 public void sort() { 33 /*忽律并发问题*/ 34 rankList.sort((o1, o2) -> { 35 RankScore r1 = rankMap.get(o1); 36 RankScore r2 = rankMap.get(o2); 37 return r1.compareTo(r2); 38 }); 39 } 40 41 public void breSort() { 42 /*忽律并发问题*/ 43 rankList.sort((o1, o2) -> { 44 RankScore r1 = rankMap.get(o1); 45 RankScore r2 = rankMap.get(o2); 46 return r2.compareTo(r1); 47 }); 48 } 49 50 } 51 52 RankPack rankPack = new RankPack(); 53 54 @Test 55 public void test() { 56 init(); 57 sort(); 58 sort(); 59 sort(); 60 sort2(); 61 sort2(); 62 sort2(); 63 } 64 65 public int randomScore() { 66 return RandomUtils.random(100, 10000); 67 } 68 69 public void init() { 70 MarkTimer build = MarkTimer.build(); 71 int speed = 1; 72 for (int i = 0; i < 300; i++) { 73 rankPack.addScore(speed, randomScore()); 74 speed++; 75 } 76 rankPack.breSort(); 77 float v = build.execTime(); 78 System.out.println(rankPack.getClass().getSimpleName() + " - 数量:" + rankPack.rankMap.size() + ", 全部排序耗时:" + v); 79 show(); 80 } 81 82 public void sort() { 83 MarkTimer build = MarkTimer.build(); 84 rankPack.rankMap.values().forEach(rankScore -> rankScore.setScore(randomScore())); 85 rankPack.breSort(); 86 float v = build.execTime(); 87 System.out.println(rankPack.getClass().getSimpleName() + " - 数量:" + rankPack.rankMap.size() + ", 全部排序耗时:" + v); 88 show(); 89 } 90 91 public void sort2() { 92 MarkTimer build = MarkTimer.build(); 93 RankScore randomItem = (RankScore) RandomUtils.randomItem(rankPack.rankMap.values()); 94 randomItem.setScore(randomScore()); 95 rankPack.breSort(); 96 float v = build.execTime(); 97 show(); 98 int index = rankPack.rankList.indexOf(randomItem.getUid()); 99 System.out.println(v + " - " + randomItem + " - 当前排名:" + (index + 1)); 100 } 101 102 public void show() { 103 // AtomicInteger atomicInteger = new AtomicInteger(); 104 // for (int i = 0; i < 10; i++) { 105 // Long uid = rankPack.rankList.get(i); 106 // RankScore rankScore = rankPack.rankMap.get(uid); 107 // System.out.println(rankScore.toString() + " - 排名:" + (i + 1)); 108 // } 109 } 110 111 } 运行一下看看效果 视乎性能还可以,等等,我们是不是忽律一个问题,现在排行榜只有300个对象哦,加到3万试试 很明显看得出来,全排序情况下直接翻了30倍左右;单个项修改后排序直接翻了差不多40倍; 到这里或许有很多已经满足需求了,; 可是我的需求远不止如此,这很明显时间消耗太久了,不知道有么有更为适合的方案来处理性能; 突然想到一个问题,或许了解树结构的同学知道,树装结构数据,在插入数据的时候就已经排序了,并且根据hash索引,性能非常高; 由此我们想到, 那么我们排序很耗时;我能不能不做这个排序操作,通过索引数据结构来达到目的呢? 改造树装结构 我们把刚才hashmap改为treemap 吧list改为treeset试试效果 测试一下,感觉确实是快了很多呢 我们来打印一下数据看看情况 哦豁,为什么数据不对; 不是我们预期的效果, 然后研究了treeset数据结构就知道,它是排序的,但是他是add的时候排序的,一旦add就不再变更了; 那我们能不能尝试修改的时候先移除,再修改,然后在add呢? 我们来改造一下addScore方法块; 排序正常了 可以看出运行结构,整体差距不算特别大; 如果考虑并发性能问题;可以把 TreeMap 换成 ConcurrentSkipListMap TreeSet 换成 ConcurrentSkipListSet java自带的跳表结构,添加删除查询,都会非常高效; 最后粘贴一下最新的全部测试代码 1 package test; 2 3 import org.junit.Test; 4 import org.wxd.lang.RandomUtils; 5 import org.wxd.lang.rank.RankScore; 6 import org.wxd.system.MarkTimer; 7 8 import java.util.concurrent.ConcurrentSkipListMap; 9 import java.util.concurrent.ConcurrentSkipListSet; 10 11 /** 12 * @author: Troy.Chen(無心道, 15388152619) 13 * @version: 2022-12-08 20:48 14 **/ 15 public class RankPackTest { 16 17 public class RankPack { 18 ConcurrentSkipListMap<Long, RankScore> rankMap = new ConcurrentSkipListMap<>(); 19 ConcurrentSkipListSet<RankScore> rankList = new ConcurrentSkipListSet<>(); 20 21 public void addScore(long uid, double score) { 22 /*忽律并发问题 可以自行改为 ConcurrentHashMap*/ 23 RankScore score1 = rankMap.computeIfAbsent(uid, l -> { 24 RankScore rankScore = new RankScore().setUid(uid); 25 return rankScore; 26 }); 27 rankList.remove(score1); 28 score1.setScore(score); 29 rankList.add(score1); 30 } 31 32 // public void sort() { 33 // /*忽律并发问题*/ 34 // rankList.sort((o1, o2) -> { 35 // RankScore r1 = rankMap.get(o1); 36 // RankScore r2 = rankMap.get(o2); 37 // return r1.compareTo(r2); 38 // }); 39 // } 40 // 41 // public void breSort() { 42 // /*忽律并发问题*/ 43 // rankList.sort((o1, o2) -> { 44 // RankScore r1 = rankMap.get(o1); 45 // RankScore r2 = rankMap.get(o2); 46 // return r2.compareTo(r1); 47 // }); 48 // } 49 50 } 51 52 RankPack rankPack = new RankPack(); 53 54 @Test 55 public void test() { 56 sort(); 57 sort(); 58 sort(); 59 sort(); 60 sort2(); 61 sort2(); 62 sort2(); 63 sort2(); 64 } 65 66 public int randomScore() { 67 return RandomUtils.random(100, 10000); 68 } 69 70 public void sort() { 71 MarkTimer build = MarkTimer.build(); 72 int speed = 1; 73 for (int i = 0; i < 30000; i++) { 74 rankPack.addScore(speed, randomScore()); 75 speed++; 76 } 77 // rankPack.breSort(); 78 float v = build.execTime(); 79 System.out.println(rankPack.getClass().getSimpleName() + " - 数量:" + rankPack.rankList.size() + ", 全部排序耗时:" + v); 80 show(); 81 } 82 83 public void sort2() { 84 MarkTimer build = MarkTimer.build(); 85 RankScore randomItem = (RankScore) RandomUtils.randomItem(rankPack.rankMap.values()); 86 rankPack.addScore(randomItem.getUid(), randomScore()); 87 // rankPack.breSort(); 88 float v = build.execTime(); 89 show(); 90 int index = 0; 91 for (RankScore rankScore : rankPack.rankList) { 92 if (rankScore.getUid() == randomItem.getUid()) break; 93 index++; 94 } 95 System.out.println(v + " - " + randomItem + " - 当前排名:" + (index + 1)); 96 } 97 98 public void show() { 99 int i = 0; 100 for (RankScore rankScore : rankPack.rankList) { 101 System.out.println(rankScore.toString() + " - 排名:" + (i + 1)); 102 i++; 103 if (i >= 10) break; 104 } 105 } 106 107 } 大家如果有兴趣可以自己测试哦; 好了这里提供集中思路来处理排行榜相关的数据;以及排名功能; 不知道园子里的朋友们,还有没有更好的思路; 听说大家喜欢看,机会是留给有耐心的人
2024-01-19
238
0
0
网络聚合
2024-01-19
C#开发PACS医学影像三维重建(十四):基于能量模型算法将曲面牙床展开至二维平面
在医学影像领域中,将三维重建中的人体组织展开平铺至二维,用来研判病灶和制定治疗方案的重要手段之一, 它能够将立体曲面所包含的信息更为直观的展示到二维平面上,常用的情景包括: 牙床全景图、平铺血管、骨骼二维化展开(肋骨平铺)。 众所周知,人体牙床正常情况下是有弧度的,无论是从俯视位还是冠状位观察都是不能直观的了解牙齿状况, 或多或少的都会被其他组织或牙齿遮挡,如下图所示: 所以我们要将三维或二维的影像拉伸后平铺到桌面上,目前主流曲面展开算法有如下几种: ①元素法 ②旋转正交矩阵法 ③迭代应变能量释放法 本文将根据网络查询现有的算法粗略介绍用能量法展开牙床: 基于弹簧质点系统建立能量模型: 弹性变形能E和弹性力f的计算式为: 判断展开标准: 曲面展开算法示例: 以VTK中圆柱体为例,将一根圆柱展开为一个矩形平面的部分代码: void Cylinder_Expansion(vtkPolyData* srcData, vtkPolyData* destData, GEO_CYLINDER src_Cylinder) { vtkSmartPointer<vtkPoints>srcPoints = srcData->GetPoints(); vtkSmartPointer<vtkPoints>destPoints = vtkSmartPointer<vtkPoints>::New(); int num = srcPoints->GetNumberOfPoints(); double p[3],r[3],cross[3]; double v0[3] = { 0 }, v1[3] = {0}; v0[src_Cylinder.RdTran] = src_Cylinder.CenterTran; v0[src_Cylinder.RdLong] = src_Cylinder.CenterLong; v1[src_Cylinder.RdTran] = src_Cylinder.R; v1[src_Cylinder.RdLong] = 0; double arc_len; for (int i = 0; i < num; ++i) { srcPoints</span>-><span style="color: rgba(0, 0, 0, 1)">GetPoint(i, p); v0[src_Cylinder.Axial]</span>=<span style="color: rgba(0, 0, 0, 1)"> p[src_Cylinder.Axial]; vtkMath::Subtract(p, v0, p); arc_len </span>=<span style="color: rgba(0, 0, 0, 1)"> vtkMath::AngleBetweenVectors(v1,p); vtkMath::Cross(v1, p, cross); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (cross[src_Cylinder.Axial]<<span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">)<br> arc_len </span>= vtkMath::Pi()*<span style="color: rgba(128, 0, 128, 1)">2</span>-<span style="color: rgba(0, 0, 0, 1)">arc_len; r[src_Cylinder.RdTran]</span>= src_Cylinder.CenterTran+ arc_len*<span style="color: rgba(0, 0, 0, 1)">src_Cylinder.R; r[src_Cylinder.Axial] </span>=<span style="color: rgba(0, 0, 0, 1)"> v0[src_Cylinder.Axial]; r[src_Cylinder.RdLong] </span>=sqrt(p[<span style="color: rgba(128, 0, 128, 1)">0</span>] * p[<span style="color: rgba(128, 0, 128, 1)">0</span>] + p[<span style="color: rgba(128, 0, 128, 1)">1</span>] * p[<span style="color: rgba(128, 0, 128, 1)">1</span>] + p[<span style="color: rgba(128, 0, 128, 1)">2</span>] * p[<span style="color: rgba(128, 0, 128, 1)">2</span>]) -<span style="color: rgba(0, 0, 0, 1)"> src_Cylinder.R;</span> destPoints-><span style="color: rgba(0, 0, 0, 1)">InsertPoint(i,r); }</span> destData-><span style="color: rgba(0, 0, 0, 1)">SetPoints(destPoints); } 运行结果: 将此算法应用到医学影像中,开始定位展开锚点: 现在就可以在同一屏画面中得到了各种视角的牙床图: 同理,还可以应用到血管和骨骼的平铺展开,更好的观察血管阻塞和破裂、骨折骨裂等情况。
2024-01-19
387
0
0
网络聚合
2024-01-19
算法 | 数字图像处理之「中值滤波」
中值滤波原理 中值滤波就是用一个奇数点的移动窗口(要求奇数主要是为了保证整个模板有唯一中心元素),将窗口中心点的值用窗口内各点的中值代替。假设窗口内有5点,其值为80、90、200、110和120,那么此窗口内各点的中值即为110。 设有一个一维序列\(f_1,f_2,...,f_n\),取窗口长度(点数)为m(m为奇数),对其进行中值滤波,就是从输入序列中相机抽出m个数\(f_{i-v},...,f_{i-1},f_i,f_{i+1},...,f_{i+v}\)(其中\(f_i\)为窗口中心点值,\(v=(m-1)/2\)),再将这m个点按其数值大小排序,取其序号为中心点的那个数作为滤波输出。用数学公式表示为: \[y_i=Median\{f_{i-v},...,f_{i-1},f_i,f_{i+1},...,f_{i+v}\},i\in N,v=\frac{m-1}{2} \] 如:以3*3的领域为例求中值滤波中像素5的值。 int pixel[9]中存储像素1,像素2...像素9的值; 对数组pixel进行排序操作; 像素5的值即为数组pixel的中值pixel[4]。 代码实现 void medianFilter(cv::Mat& src, cv::Mat& dst, cv::Size size) { /*step1:判断窗口size是否为奇数*/ if (size.width % 2 == 0 || size.height % 2 == 0) { cout << "卷积核窗口大小应为奇数!\n"; exit(-1); } /*step2:对原图进行边界扩充*/ int h = (size.height - 1) / 2; int w = (size.width - 1) / 2; Mat src_border; copyMakeBorder(src, src_border, h, h, w, w, BORDER_REFLECT_101); /*step3:卷积操作*/ map<uchar, Point> mp; // 定义容器存储每个卷积窗口内各像素点的<像素值, 像素位置> for (int i = h; i < src.rows + h; i++) { for (int j = w; j < src.cols + w; j++) { mp.clear(); for (int ii = i - h; ii <= i + h; ii++) { for (int jj = j - w; jj <= j + w; jj++) { Point point(jj, ii); uchar value; if (src.channels() == 1) { // 灰度图像,存储灰度值 value = src_border.at<uchar>(ii, jj); }else { // 彩色图像,存储亮度值 uchar value_b = src_border.at<cv::Vec3b>(ii, jj)[0]; uchar value_g = src_border.at<cv::Vec3b>(ii, jj)[1]; uchar value_r = src_border.at<cv::Vec3b>(ii, jj)[2]; value = 0.114 * value_b + 0.587 * value_g + 0.299 * value_r; } mp[value] = point; } } // 将窗口中心点的值用窗口内个点的中值代替 auto iter = mp.begin(); int count = 0; Point pixel; int median_size = mp.size() / 2; while (iter != mp.end()) { if (count == median_size) { pixel = Point(iter->second.x, iter->second.y); break; } count++; iter++; } if (src.channels() == 1) { dst.at<uchar>(i - h, j - w) = src_border.at<uchar>(pixel.y, pixel.x); } else { dst.at<cv::Vec3b>(i - h, j - w)[0] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[0]; dst.at<cv::Vec3b>(i - h, j - w)[1] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[1]; dst.at<cv::Vec3b>(i - h, j - w)[2] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[2]; } } } } 代码讲解 copyMakeBorder(src,src_border,h,h,w,w,BORDER_REFLECT_101); 在模板或卷积的加权运算中的图像边界问题:当在图像上移动模板(卷积核)至图像边界时,在原图像中找不到与卷积核中的加权系数相对应的N个像素(N为卷积核元素个数),即卷积核悬挂在图像的边界上,这种现象在图像的上下左右四个边界上均会出现。例如,当模板为: \[\frac{1}{9} \begin{bmatrix} %该矩阵一共3列,每一列都居中放置 1 & 1 & 1\\ %第一行元素 1 & 1 & 1\\ %第二行元素 1 & 1 & 1\\ %第二行元素 \end{bmatrix} \] 设原图像为: \[\begin{bmatrix} %该矩阵一共3列,每一列都居中放置 1 & 1 & 1 & 1 & 1\\ %第1行元素 2 & 2 & 2 & 2 & 2\\ %第2行元素 3 & 3 & 3 & 3 & 3\\ %第3行元素 4 & 4 & 4 & 4 & 4\\ %第3行元素 \end{bmatrix} \] 经过卷积操作之后图像为: \[\begin{bmatrix} %该矩阵一共3列,每一列都居中放置 - & - & - & - & -\\ %第1行元素 - & 2 & 2 & 2 & -\\ %第2行元素 - & 3 & 3 & 3 & -\\ %第3行元素 - & - & - & - & -\\ %第3行元素 \end{bmatrix} \] "-"表示无法进行卷积操作的像素点。 解决方法有2种:①忽略图像边界数据(即不管边界,卷积操作的范围从整张图缩小为边界内缩K圈,K的值随卷积核尺寸变化)。②将原图像往外扩充像素,如在图像四周复制源图像边界的值,从而使得卷积核悬挂在原图像四周时也能进行正常的计算。 opencv边框处理copyMakeBorder: https://zhuanlan.zhihu.com/p/108408180 value=0.114*value_b+0.587*value_g+0.299*value_r; 对于彩色图像,我们取图像亮度的中间值,亮度值的计算方法为: \[luminance = 0.299R + 0.587G + 0.114B \] map<uchar, Point> mp; map为C++的stl中的关联性容器,为了实现快速查找,map内部本身就是按序存储的(map底层实现是红黑二叉树)。在我们插入<key, value>键值对时,就会按照key的大小顺序进行存储,其中key的类型必须能够进行 < 运算,且唯一,默认排序是按照从小到大遍历。 因此,将亮度值或灰度值作为键,map将自动进行按键排序,无需手动书写排序代码。 实现效果 卷积核size为(5, 5)。
2024-01-19
275
0
0
网络聚合
2024-01-19
Django笔记二十之手动编写migration文件
本文首发于公众号:Hunter后端 原文链接:Django笔记二十之手动编写migration文件 前面介绍过,migration 文件主要记录的是 Django 系统 model 的变化,然后通过 migrate 命令将变化适配到数据库中。 比如在某个 application 下新增了某张表,或者对某张表更改了字段,可以生成 migration 文件,然后通过 migrate 更改到数据库。 除了系统能够自动生成的,我们还可以手动创建 migration 文件来操作数据库,这个用途主要是用于比如,创建表后,需要写入一些初始化的数据的情况。 基础命令 migration文件介绍 自定义migration文件 RunSQL() RunPython() 1、基础命令 关于 migration 的命令有如下几条: makemigrations migrate sqlmigrate showmigrations 其中 前面三条命令在第二篇笔记中已经介绍过使用方法,这里介绍一下 showmigrations。 这个作用主要是查看某个 application 下的migration 文件是否已经被更改到数据库中,可以在 Django 系统的根目录用下面的命令测试: python3 manage.py showmigrations blog 可以看到下面的输出: blog [X] 0001_initial [X] 0002_auto_20220118_0926 [X] 0003_auto_20220121_1016 其中,前面的 [X] 表示已经被更改到数据库中,如果我们再对 blog 的 model 进行任意修改,然后执行 makemigrations 的操作,再次执行 showmigrations 的操作,可以看到下面的输出: blog [X] 0001_initial [X] 0002_auto_20220118_0926 [X] 0003_auto_20220121_1016 [ ] 0004_alter_book_price 可以看到最下面的一条记录 [] 中是没有 X 的,表示这条 migration 文件没有被执行 migrate。 2、migration文件介绍 每一次通过 makemigrations 生成的 migration 文件都存在系统中,一个最基础的 migration 文件像下面这样: from django.db import migrations, models class Migration(migrations.Migration): dependencies = [('blog', '0001_initial')] operations = [ migrations.DeleteModel('Tribble'), migrations.AddField('Author', 'rating', models.IntegerField(default=0)), ] 一个 Migration 的类下,有两个参数,一个是 dependencies,一个是 operations dependencies 作用是定位上一个执行的 migration 文件的地方,因为每一次 migrate 的执行都是按照顺序的 且他的参数是一个列表,列表的元素是一个元组,里面有两个参数,一个是 application 的名称,一个是上一次运行的 migration 文件,他是可以指定到多个 application 的,意义为在某两个 application 的 migration 文件之后再执行 operations 的作用是 migration 里需要执行的操作,可以是字段的增加、删除、修改、也可以是表的创建和删除 一个 migration 在执行 migrate 前,我们可以手动对其修改,甚至可以完全自己来定义 3、自定义migration文件 前面介绍了 migration 文件的基本结构,其中有一些关于字段和 model 的操作方法,这些操作都可以通过 makemigration 的方式自动生成。 我们自定义的 migration 文件,与上面的保持一致即可,自定义的 migration 文件需要修改的地方是 operations 里的元素。 假设我们有这样一个需求,创建一张基础映射表后,里面是系统运行所必需的数据,需要在创建表后立即写入,那么就用到了我们这个自定义的 migration 文件。 除了对表字段或者表的修改,还有两种方法实现数据的写入, 一种是使用 SQL 语句插入,用到的migration的函数是 RunSQL() 一种是使用 Django 的 ORM 语句,写 python 的函数来插入,函数是 RunPython 假设创建 Blog 表的migration file 是 0001_create_blog.py 现在需要对其插入两条数据,name 和 tagline 分别是 ('name_1', 'tagline_1') 和 ('name_2', 'tagline_2') 下面用 RunSQL() 和 RunPython() 两种方式来分别介绍。 4、RunSQL() RunSQL() 函数接受一个字符串,或者一个数组作为参数,参数的内容都是 SQL 语句,这也是为什么函数名为 RunSQL()。 字符串的形式为完整的 SQL 语句,比如我们需要插入这两条数据,则是: migrations.RunSQL( "INSERT INTO blog_blog (name, tagline) values('name_x_4', 'tagline_1'), ('name_x_5', 'tagline_2');" ) 如果是作为数组传入,形式则是: migrations.RunSQL( sql=[ ( "INSERT INTO blog_blog (name, tagline) values(%s, %s), (%s, %s);", ['name_x_6', 'tagline_1', 'name_x_7', 'tagline_2'] ) ] ) 在数组的传入形式中,我们将需要插入的数据都放到一个数组中传入 reverse_sql RunSQL() 函数除了 sql 参数,还有一个 reverse_sql 参数,用途是 sql 参数执行的 SQL 语句没有执行成功的情况下的一种操作,一般是用于防止数据污染。 假设说我们的 sql 为插入数据,但是因为某种原因,这条语句没有正确插入,报错了,那么系统就会执行 reverse_sql 中的语句,作为一个可逆的操作。 以下是官方的一个使用示例: migrations.RunSQL( sql=[("INSERT INTO musician (name) VALUES (%s);", ['Reinhardt'])], reverse_sql=[("DELETE FROM musician where name=%s;", ['Reinhardt'])], ) 5、RunPython() RunSQL() 函数操作的是 SQL 语句,RunPython() 参数则是 Python 函数,可以将我们需要写入的数据都写到函数的步骤里,然后在 RunPython() 中调用 以下是使用示例: def insert_blog_data(apps, schema_editor): Blog = apps.get_model("blog", "Blog") db_alias = schema_editor.connection.alias Blog.objects.using(db_alias).create(name="name_3", tagline="tagline_3") Blog.objects.using(db_alias).create(name="name_4", tagline="tagline_4") class Migration(migrations.Migration): dependencies = [ ("blog", "0001_initial"), ] operations = [ migrations.RunPython(insert_blog_data) ] 其中,insert_blog_data 是需要执行的函数,在这个函数里,有两个默认参数,apps 和 schema_editor apps 可以用来获取我们需要的 model,根据函数 apps.get_model(), 这个函数传入两个参数,一个是 application,我们这里是 blog, 一个 model 的名称,我们这里是 Blog 而 schema_editor 则是可以用于获取数据库的 alias 然后,数据的插入的方式就和普通的 model 的操作方法一致了。 RunPython() 函数和 RunSQL 一样,也可以输入两个参数,第二个参数作用也是用于操作失败的回退操作: migrations.RunPython(insert_blog_data, reverse_insert) 以上就是介绍 migration 的全部内容了,下一篇笔记将介绍如何在 Django 中使用原生的 SQL 来查询数据。 如果想获取更多后端相关文章,可扫码关注阅读:
2024-01-19
343
0
0
网络聚合
2024-01-19
vivo 游戏中心低代码平台的提效秘诀
作者:vivo 互联网服务器团队- Chen Wenyang 本文根据陈文洋老师在“2022 vivo开发者大会"现场演讲内容整理而成。公众号回复【2022 VDC】获取互联网技术分会场议题相关资料。 在互联网流量见顶和用户需求分层的背景下,如何快速迭代产品功能,满足用户需求成为了开发首要面对的问题。游戏中心低代码平台从产品定位入手,以组件化方式搭建用户端页面,快速支撑产品需求,提升了研发效率,缩短了项目周期。本文首先介绍背景与痛点,然后阐述了游戏中心是如何搭建低代码平台,最后展示了低代码平台带来的收益和未来建设方向。 一、背景介绍与痛点分析 vivo游戏是vivo用户玩游戏的平台,其主要产品形态是vivo游戏中心以及vivo游戏内置悬浮球,它为用户提供了找游戏,玩好游戏,找人一起玩游戏的价值。vivo游戏中心是vivo游戏的核心流量入口,因此游戏中心首页就承担了非常重要的角色。首页的风格延续了好几年,基础样式几乎没有什么变化,强调分发。随着时间的发展,各种问题就慢慢突显出来了。 1.从2020年开始,互联网流量见顶,分发提升困难,需要探索新方向,而应对新需求的时候研发周期过长。从需求评审到功能上线,灰度到全量,需要耗时一个月以上,运营效果往往低于预期。 2.核心用户的关注点不同。MOBA玩家看画面和平衡,传奇玩家看游戏人数,消消乐玩家看玩法,且用户对游戏福利活动的需求也是非常强烈。首页列表中,重点信息无法突出,也无法给用户带来强烈的下载冲动。 3.任何一个游戏都是有生命周期的。在不同的阶段,突出的重点是不一样的。预约的时候可能突出这个游戏的画风和画质,重点更新的时候可能突出的是新玩法。游戏中心首页没有相关的位置或者手段来突出这些信息。 4.无法快速响应运营或者开发者诉求。如果运营需要更换首页跳转的二级落地页,或者响应开发者诉求搭建一个特殊专区的时候,都是需要开发的,现有功能无法快速支撑。 这几个问题是表层的问题,透过现象看本质,我们可以归纳出,游戏中心缺少了两项基础能力。一方面,游戏中心缺少灵活多样,且能动态调整的组件化能力;另一方面,游戏中心缺少可视化,快速搭建页面的能力。基于以上痛点,结合行业前沿知识,笔者所在团队商量决定,利用低代码思想,打破原有秩序,重新搭建新平台。 二、如何建设游戏中心低代码平台 大家可能会好奇,低代码平台一般都是通用性比较强的平台,怎么能和业务属性如此鲜明的游戏中心结合呢?那接下来笔者为大家一一道来。 低代码平台离不开组件化设计,那什么是组件化设计呢?组件化设计是指针对相同或者不同功能,性能,规格的产品,进行功能分析,设计出一系列的功能组件。通过组件的多样选择将产品客制化,以满足不同的市场需求。由此可以推出,游戏中心组件化设计就是针对游戏中心进行功能分析,设计出一系列功能组件,通过组件的多样化选择,快速搭建出不同的页面,以满足不同用户的需求。 那么,我们是怎么来定义游戏中心的组件呢? 在原有系统的基础上,结合游戏中心app各个位置的形态以及未来定位,把游戏中心首页按照横向划分,每一行细化为一个组件。虽然大家可能不太了解游戏中心,但是对于市面上大部分分发类的产品来说,它们每个页面里面的UI样式是系列化的,比如视频样式,图片样式等,变化比较多的是内容,所以我们可以以行来定义组件。另一方面,组件粒度的粗细,和组件的灵活性成反向关系,但是和运营的配置能力成正向关系,即组件粒度越细,越基础,那么组件的灵活度就越高,运营的配置配置成本也越高。所以,选择什么粒度的基础组件,是需要结合实际业务需求,综合分析后确定的。不是粒度越细越好,也不是粒度越粗越好。 确定好组件的粒度后,我们通过一个例子来详解拆分一下组件的构成。大家请看图1,这是一个专题组件。其形式为左上是标题,右上是跳转二级页的按钮,多个游戏横向并列成一行,最下面是安装按钮。将这种组合形式抽象为一个多游戏并列(1*4)展示的基础模板,基础模板同时也叫元组件,那么就可以把这类基础模板命名为专题元组件。运营希望该元组件可以配置标题,跳转链接,行数和展示游戏个数。这些是静态的基础配置。同时,专题组件需要配置一个数据源,这个数据源决定专题里面游戏的内容和展示顺序,这个数据可以是运营配置的或者实时推荐的。例如,在推荐场景下,用户发起请求的时候,可以由推荐实时返回,那么这就是动态数据。Banner同理。因此可以认为:组件是由元组件和数据构成的。 图1 那么元组件是怎么定义的呢?在后台,我们可以新增一个元组件,填写完卡片编号名称描述等信息后,再上传图片保存。这里的元组件卡片编号作为该组件的唯一标识,是有相应的业务含义,由该编号确定当前组件展示数据的格式,即通过编号确定处理数据的流程类。上传图片主要方面运营配置页面。因为在页面搭建过程中,运营配置多个组件的时候,后台会实时显现客户端的效果。展示的组件图片就是在此处上传的。 图2 配置完元组件的基础信息后,在元组件配置管理的左边,如图3,通过编辑schema,右边就会出现当前元组件能够配置的标题,跳转链接,行数以及单行游戏的个数。这些配置是运营在配置组件的时候可以动态配置的;配置完这些数据后,在页面管理后台,添加该类组件的时候,红框中出现的就是我们能够配置的基础属性了,如图4。到这一步,元组件的配置就完成了。 图3 图4 接下来就是数据源的配置。在运营点击数据选择的时候(图5),弹出的就是动态数据的配置,那这些数据是怎么来的呢? 我们需要从两个维度来看数据:第一个,数据有哪些;第二个,数据怎么交互。我们从两个角度来看数据有哪些。从数据类型角度划分,有运营配置数据和系统自动数据。运营配置数据为运营人员为了达到某一个目的而手动配置或者干预的数据,而系统自动数据为从系统某一个源自动获取的数据,不需要人工介入。从数据来源划分,有内部数据和外部数据。通过不同的数据类型和来源,我们切分为不同的调用方式,这样能最大限度地保证系统的扩展性和维护性。 随着业务的发展,平台会不断吸取其他业务数据,来丰富当前业务的形态,但是获取外部数据的方式只有两种:http和dubbo协议。通过这两个协议的配合,能够标准化获取外部数据。我们说完了元组件和数据,那么他们是怎么绑定的呢?在后台数据管理中,我们会按照某个运营目的,来确定一个组件的应用场景,比如专题组件的应用场景就是为用户推荐某一类型的游戏集合。通过定义组件的应用场景,我们把元组件和数据绑定在一起。 整体过程如下: 确定组件的应用场景名和编号; 选择一个或者多个元组件; 确定数据源类型,调用类型和数据业务方; 确定调用的http和dubbo接口。通过http接口,可以生成运营能够配置的数据,即点击选择后弹出的列表,点击选择后,即可将数据绑定到组件上,如图5;在用户调用流程中,通过dubbo接口,利用后台配置的数据,可以请求获取到更加详细的数据。 此时一个组件就配置完成了。 图5 在前台数据的调用方式中,使用了阿里的QLExpress。QLExpress由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。它的特性优势和运行原理可以在GitHub上找到,在此不赘述,感兴趣的同学可以自行搜索。利用其弱类型脚本的特性,将运营配置的数据转换成调用外部接口的参数,通过dubbo泛化调用技术,获取到具体的数据。同时,还是利用弱类型脚本的特性,转换返回结果,控制业务逻辑和数据范围。采用QLExpress和dubbo泛化调用的方式,可以减少代码开发,增加数据灵活性。 最后,来了解一下整个页面的配置过程。前面讲述到,元组件是最基础的组件,通过元组件,我们配置一些基础信息,并关联一些动态数据,构成了一个组件。通过把多个组件拖拽到页面上,就可以实现运营配置生成页面的效果。同时,页面也可以直接拖拽已经配置好的组件,这就是一个组件被多个页面引用的情况,实现了组件级的复用。在页面之上,我们还引入了方案的概念。方案,即多个页面的集合。通过页面的组合,首页可以实现多个页面的展示,既能展示游戏中心的门户,又能个性化运营。 如图6,从下到上,通过数据和元组件,可以构成一个组件,通过多个组件的选择,可以构成一个页面,多个页面构成一个方案。如图7,从上到下,通过多层实验框架,确定需要展示给用户的方案。接下来,通过dmp用户画像,确定展示个性化的页面。每一个页面都是由若干个组件构成的。每个组件是由元组件和数据构成。 图6 图7 三、成果展示 罗马不是一天建成的,游戏中心低代码平台也不是一蹴而就的。平台20年就上线了,由于缺少运营场景,功能也不是很完善,能够带来的效益微乎其微,甚至内部也产生过质疑,是不是不值得花这么多的时间精力建设平台,但经过时间的沉淀,游戏中心低代码平台的效果愈发明显。 首先,研发流程和原先不一样了。当我们在新增/修改组件的时候,客户端同学通过flutter等动态化技术,完成新组件的开发修改,并且在后台上传flutter的更新包或者差分包。 服务器同学需要在后台配置元组件的信息,配置组件应用场景,绑定元组件和数据关系,就可以生成运营可以配置的组件。运营配置完组件,页面,方案,点检完毕审核通过后就可以上线了,如图8。 图8 其次,研发效率提升了。大家注意到,最大的一个变化是,客户端不需要发版了。在一些特殊场景下,服务器也不需要开发。对比原先的研发流程,效率发生了质的飞越。针对不同的角色,提升的效率是不一样的;对于客户端来说功能全量上线周期可缩短15天以上,有较高的容错性,对于服务器来说,开发效率提升4倍以上,对于测试来说,无需回归老版本,测试效率提升30%-50%;对于运营来说,可视化的操作降低30%的学习成本,提升10%的配置效率。 最后,项目周期缩短了。原先如果运营做一个功能,首先得把需求提给产品(其实在提需求之前,还有一个需求讨论的过程,非需求评审),再进行需求评审,评审完毕后需要根据各个需求的优先级进行排序。而此类需求由于效果不明显,且论证数据不好收集,往往其优先等级就比较低。需求评审完毕后,还需要策划评审,概要设计评审等等诸多流程,上线完毕还需要灰度一周,有了上线报告之后才可以全量。但是,有了低代码平台后,流程就没有这么复杂了。最简单的流程,无需更改组件,运营自己就能操作。还有一些简单的场景,服务器修改配置就能完成组件的修改。最复杂的就是全新场景,但由于之前的基础在,开发效率也是非常的高。整体流程至少可以缩短为原来的1/4。接下来用一个例子来说明一下。 图9 图10 四、未来展望 游戏中心低代码平台的建设标准,和通常意义上低代码平台的建设存在差异。游戏中心低代码平台由“游戏中心业务”衍生,慢慢演变到可以适配vivo生态内分发类app的终端解决方案。这符合我们的业务发展,也为低代码的演变提供了养分。通过不断的适配和演变,我们希望能够将低代码的解决方案普惠安卓生态。因此,在未来的建设思路上,我们的目标是能够解放生产力,提升用户体验,做最好用的安卓低代码平台。 五、总结 低代码的概念最近很火,争议也很大。有人认为以后“人人都是程序员”,也有人认为是新瓶装旧酒。但作为技术人,最重要的还是通过技术解决业务问题,驱动业务发展。游戏中心低代码平台旨在提高开发效率,帮助业务取得更好的结果。未来,我们也会投入更多的精力优化系统,不断为用户创造惊喜,为行业带来革新。
2024-01-19
234
0
0
网络聚合
2024-01-19
【机器学习】李宏毅——AE自编码器(Auto-encoder)
1、What 在自编码器中,有两个神经网络,分别为Encoder和Decoder,其任务分别是: Encoder:将读入的原始数据(图像、文字等)转换为一个向量 Decoder:将上述的向量还原成原始数据的形式 而目标是希望还原出来的结果能够与原始数据尽可能的接近。其中的向量可称为Embedaing、Representation、Code。而它的主要用处就是将原始数据(高维、复杂)经过Encoder后得到的向量(经过处理,低纬度)作为下游任务的输入。 2、Why 因为例如图像这种原始数据它的变化是有限的(不可能每一个像素点都是完全随机的,这不是我们可能看到的图片),因此如果AutoEncoder能够找到它们之间的变化规律(通常是比原始数据更简单的)那么就可以用更加简便的表达形式来表示数据,那么在下游任务训练的时候就可能可以用更简单的数据、更少的数据来学习到原来想要让机器学习到的东西了。 3、De-noising Auto-encoder 这个和普通的Auto-encoder的区别在于,Encoder的输入并不是原始的图像,而是将图像加上一定的噪声之后再作为Encoder的输入,而在输出的时候是要求Decoder输出能够与未加噪声之前的图像越接近越好,即: 而如果我们回顾一下之前学习过的BERT,可以发现BERT实际上就是De-noising Auto-encoder,可以看下图: 4、Feature Disentangle 特征区分技术可以用于上文介绍的Auto-encoder,具体上可以这么理解:在Auto-encoder中我们将图片、文字、语音等放入Encoder得到的输出向量Embedaing中就包含了这些输入的特征信息,但是一个输入可能存在不同的特征信息,例如一段语音就包含语音的内容、说话者的特征等等,那么有没有可能在Embedaing中将这些特征分别提取出来呢?这就是Feature Disentangle想要实现的事情。 5、Voice Conversion 语者转换这个例子就是学习完模型之后,将A说话的内容用B的声音复述出来作为输出,就好像柯南的领带变声器一般神奇。那么Auto-encoder如何来实现这个任务呢? 实际上这就需要借助Feature Disentangle。首先如果将该任务作为一个监督学习的任务,那我们就需要A和B两个人分别来说同样的句子同样的内容,产生大量的样本从而来进行训练,但是这显然是不可能的!因此如果我们利用Auto-encoder和Feature Disentangle,可以有这样的思路: 训练完Auto-encoder后,将A说话的语音和B说话的语音都输出Encoder得到对应的Embedaing输出 运用特征提取技术,将A和B对应的Embedaing分别提取出说话的内容和语者的特征两部分 再将A说话的特征和B的特征互换,让B的特征和A的内容拼接在一起,这样就实现了用B语者来说出A的内容。 6、Discrete Representation 上述我们说到的Embedaing是一个向量,其中每一个维度都是可以连续变化的数值。那么有没有可能我们强迫这个Embedaing是用离散的数值来表示呢?例如表示为二进制,只有0和1,每个维度表示是否含有某个特征;或者表示为One-hat-vector,来表示对物品的分类(这样就不需要标签)了,因为在学习的过程中就会自动将类似的物品归于同一类,就类似于聚类算法了。 那么这种想法比较有代表性的技术为VQVAE,其具体的流程为: 将输入经过Encoder之后得到Embedaing,然后现在有一排向量Codebook(里面向量的个数也是你指定的) 将Embedaing逐一与Codebook中的向量进行计算相似度,并取其中相似度最高的来作为Decoder的输入 训练的时候我们会要求Decoder的输出要与Encoder的输入越接近越好,从而来不断地改进Codebook中的各个向量 这样最终的结果就是让你Decoder的输入是离散的,只能在Codebook中进行选取,而且例如应用在语音的例子中,有可能最终学习得到的Codebook中的各个向量的不同维度可能会代表不同音标等等。 但这里我有一个问题就是如上图应用在图像上,那么训练完成后如果放入Encoder的是之前训练从未见过的图像,那么输出还能够与输入相接近吗? 7、令Embedaing是一段文字 如果天马行空一点,能否让Embedaing是一段文字呢?例如我们给Encoder一篇文章,然后希望它输出一段文字,而Decoder再由这段文字来还原回原来的文章。那么此时这个Embedaing是否可以认为是文章的摘要呢? 如果真的将这个想法进行实现会发现:Embedaing虽然确实是一段文字,但是它经常是我们人类看不懂的文字,即在我们看来是毫无逻辑的文字无法作为摘要,但这可以认为是En和De之间发明的暗号,它们用这些文字就可以实现输入和输出的文章都极其相似。那么如果希望中间的Embedaing是我们能够看得懂的文字,我们可以加上GAN的思想,即加上一个辨别器,该辨别器是学习了很多人类写文章的句子,它能够分辨一段文字是否是人类能够理解的逻辑,那么这就会使得En不断地调整自己的输出,希望能够欺骗过辨别器,让它认为是人类写出来的句子,因此Embedaing也就越来越接近于摘要的功能了! 8、其他应用 8.1、生成器 训练完Auto-encoder后,由于Decoder是接受一个向量,生成一个输出(例如图像),那么就可以认为这个Decoder就是一个生成器,因此可以单独拿出来作为一个生成器使用: 8.2、压缩 将Encoder训练完成后它相当于接受一个输入(例如图片)然后得到向量,那么这个向量通常是低维度的,那么我们可以认为是进行了压缩,而Decoder就是进行了解压缩。但需要注意的是由于De输出的结果无法与原始的输入一模一样,因此这样的压缩是有损的。 8.3、异常检测 我们如果想要做一个异常检测系统,那我们需要很多的资料来进行训练,而在某些应用场景中很可能我们只有非常多的正常的数据而只有非常少的异常数据,甚至于说有些异常的数据混杂在正常的数据中都分辨不出来,那么这时候Auto-encoder就可以派上用场了!如下图,我们先用正常的数据来训练我们的Auto-encoder,例如正常的数据是人脸: 那么训练完成之后,如果你进行检测时输入的也是相似的人脸,那么Auto-encoder就有较大的可能,使得输入与输出之间较为接近,即计算相似度就会较大;但是如果输入不是人脸,例如动漫人物,那么因为Auto-encoder没有看过这样的图片因此很难正确的将其还原,那么再计算输入与输出之间的相似度时就会较小,即:
2024-01-19
524
0
0
网络聚合
2024-01-19
学习.NET MAUI Blazor(三)、创建.NET MAUI Blazor应用并使用AntDesignBlazor
大致了解了Blazor和MAUI之后,尝试创建一个.NET MAUI Blazor应用。 需要注意的是: 虽然都叫MAUI,但.NET MAUI与.NET MAUI Blazor 并不相同,MAUI还是以xaml为主,而MAUI Blazor则是以razor为主。 这个系列还是以MAUI Blazor为主,要创建一个MAUI Blazor应用,需要安装Visual Studio 2022 17.3 或更高版本,并在安装程序上,勾选.NET Multi-platform App UI 开发!最好是升级到最新的.NET 7。 创建.NET MAUI Blazor应用 打开Visual Studio 2022,选择创建新项目 在搜索框输入MAUI,选择.NET MAUI Blazor应用,点下一步! 给项目起一个好听的名字,选择项目存在的位置,点下一步! 选择目标框架,这里选择的是.NET 7,点击创建。 等待创建项目及其依赖项还原。完成后的目录结构如下: .NET MAUI Blazor 需要注意的地方 .NET MAUI Blazor 运行在WebView2上,WebView2是微软推出的新一代用于桌面端混合开发的解决方案。它可以让本地应用程序(WinForm、WPF、WinUI、Win32)、移动应用程序(MAUI)轻松嵌入Web技术。WebView2 控件使用 Microsoft Edge 作为呈现引擎在客户端应用程序及App中显示 Web 内容。使用 WebView2 可以将 Web 代码嵌入到客户端应用程序及App中的不同部分,或在单个 WebView 实例中构建所有本机应用程序。 可以这么看MAUI Blazor, .NET MAUI 包含 BlazorWebView 控件,该控件运行将 Razor 组件呈现到嵌入式 Web View 中。 通过结合使用 .NET MAUI 和 Blazor,可以跨移动设备、桌面设备和 Web 重复使用一组 Web UI 组件。 说人话就是,它就是一个Hybrid App(混合应用) ! 调试.NET MAUI Blazor 在windows上调试 MAUI Blazor应用,需要Windows 10 1809及更高版本上,并打开开发者模式。 windows 11上,位于设置->隐私和安全性->开发者选项->开发人员模式 点击Windows Machine,运行程序! 如无意外,运行成功! 这时,MAUI Blazor使用的是bootstrap样式以及open-iconic图标。 在wwwroot/index.html中也可以看到 <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> 现在已经有个很多基于Blazor的组件库,所以暂时把默认的bootstrap替换成第三方组件库,这里使用的是AntDesignBlazor。 使用AntDesignBlazor 组件库 安装依赖: PM> NuGet\Install-Package AntDesign.ProLayout -Version 0.13.1 注入AntDesign 在MauiProgram.cs注入AntDesign 服务与设置基本配置,完整的MauiProgram.cs代码 using Microsoft.Extensions.Logging; using MauiBlazorApp.Data; namespace MauiBlazorApp; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); builder.Services.AddMauiBlazorWebView(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif builder.Services.AddSingleton<WeatherForecastService>(); //注入AntDesign builder.Services.AddAntDesign(); //基本配置 builder.Services.Configure<ProSettings>(settings => { settings.NavTheme = "light"; settings.Layout = "side"; settings.ContentWidth = "Fluid"; settings.FixedHeader = false; settings.FixSiderbar = true; settings.Title = "DotNet宝藏库"; settings.PrimaryColor = "daybreak"; settings.ColorWeak = false; settings.SplitMenus= false; settings.HeaderRender= true; settings.FooterRender= false; settings.MenuRender= true; settings.MenuHeaderRender= true; settings.HeaderHeight = 48; }); return builder.Build(); } } 配置项都写上了。参数含义从表达的意思就能看出来,不做注释了! 引入样式 打开wwwroot/index.html。由于我们使用的是AntDesign,所以需要改造下index.html,修改后内容如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <title>DotNet宝藏库</title> <base href="/" /> <link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" /> <link rel="stylesheet" href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" /> </head> <body> <div class="status-bar-safe-area"></div> <div id="app"> <style> html, body, #app { height: 100%; margin: 0; padding: 0; } #app { background-repeat: no-repeat; background-size: 100% auto; } .page-loading-warp { padding: 98px; display: flex; justify-content: center; align-items: center; } .ant-spin { -webkit-box-sizing: border-box; box-sizing: border-box; margin: 0; padding: 0; color: rgba(0, 0, 0, 0.65); font-size: 14px; font-variant: tabular-nums; line-height: 1.5; list-style: none; -webkit-font-feature-settings: 'tnum'; font-feature-settings: 'tnum'; position: absolute; display: none; color: #1890ff; text-align: center; vertical-align: middle; opacity: 0; -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86), -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); } .ant-spin-spinning { position: static; display: inline-block; opacity: 1; } .ant-spin-dot { position: relative; display: inline-block; font-size: 20px; width: 20px; height: 20px; } .ant-spin-dot-item { position: absolute; display: block; width: 9px; height: 9px; background-color: #1890ff; border-radius: 100%; -webkit-transform: scale(0.75); -ms-transform: scale(0.75); transform: scale(0.75); -webkit-transform-origin: 50% 50%; -ms-transform-origin: 50% 50%; transform-origin: 50% 50%; opacity: 0.3; -webkit-animation: antSpinMove 1s infinite linear alternate; animation: antSpinMove 1s infinite linear alternate; } .ant-spin-dot-item:nth-child(1) { top: 0; left: 0; } .ant-spin-dot-item:nth-child(2) { top: 0; right: 0; -webkit-animation-delay: 0.4s; animation-delay: 0.4s; } .ant-spin-dot-item:nth-child(3) { right: 0; bottom: 0; -webkit-animation-delay: 0.8s; animation-delay: 0.8s; } .ant-spin-dot-item:nth-child(4) { bottom: 0; left: 0; -webkit-animation-delay: 1.2s; animation-delay: 1.2s; } .ant-spin-dot-spin { -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); -webkit-animation: antRotate 1.2s infinite linear; animation: antRotate 1.2s infinite linear; } .ant-spin-lg .ant-spin-dot { font-size: 32px; width: 32px; height: 32px; } .ant-spin-lg .ant-spin-dot i { width: 14px; height: 14px; } .status-bar-safe-area { display: none; } @supports (-webkit-touch-callout: none) { .status-bar-safe-area { display: flex; position: sticky; top: 0; height: env(safe-area-inset-top); background-color: #f7f7f7; width: 100%; z-index: 1; } .flex-column, .navbar-brand { padding-left: env(safe-area-inset-left); } } @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { .ant-spin-blur { background: #fff; opacity: 0.5; } } @-webkit-keyframes antSpinMove { to { opacity: 1; } } @keyframes antSpinMove { to { opacity: 1; } } @-webkit-keyframes antRotate { to { -webkit-transform: rotate(405deg); transform: rotate(405deg); } } @keyframes antRotate { to { -webkit-transform: rotate(405deg); transform: rotate(405deg); } } </style> <div style=" display: flex; justify-content: center; align-items: center; flex-direction: column; min-height: 420px; height: 100%; "> <div class="page-loading-warp"> <div class="ant-spin ant-spin-lg ant-spin-spinning"> <span class="ant-spin-dot ant-spin-dot-spin"> <i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i> </span> </div> </div> <div style="display: flex; justify-content: center; align-items: center;"> <div class="loading-progress-text"></div> </div> </div> </div> <script src="_framework/blazor.webview.js" autostart="false"></script> <script src="_content/AntDesign/js/ant-design-blazor.js"></script> </body> </html> 加入命名空间 在_Imports.razor添加AntDesign命名空间: @using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using MauiBlazorApp @using MauiBlazorApp.Shared //引入AntDesign @using AntDesign 设置容器 在Main.razor中加入<AntContainer /> <Router AppAssembly="@typeof(Main).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> <!--设置容器--> <AntContainer /> 修改 MainLayout 修改MainLayout.razor。 把MainLayout.razor中默认布局删除 引入AntDesign.ProLayout 设置布局为AntDesign.ProLayout 构造菜单、页脚的链接、版权 wwwroot目录下新建个文件夹images,把提前准备好的logo放进去 完整代码如下: @using AntDesign.ProLayout @inherits LayoutComponentBase <AntDesign.ProLayout.BasicLayout Logo="@("images/logo.png")" MenuData="MenuData"> <ChildContent> @Body </ChildContent> <FooterRender> <FooterView Copyright="MauiBlazorApp" Links="Links"></FooterView> </FooterRender> </AntDesign.ProLayout.BasicLayout> <SettingDrawer /> @code { private readonly MenuDataItem[] MenuData = { new MenuDataItem { Path = "/", Name = "Home", Key = "Home", Icon = "home" }, new MenuDataItem { Path = "/Counter", Name = "Counter", Key = "Counter", Icon = "plus" }, new MenuDataItem { Path = "/FetchData", Name = "FetchData", Key = "FetchData", Icon = "cloud" } }; private readonly LinkItem[] Links = { new LinkItem { Key = "DotNet宝藏库", Title = "基于Ant Design Blazor", Href = "https://antblazor.com", BlankTarget = true } }; } 这时可以把项目中无用的内容删除掉了,如Shared/NavMenu.razor、wwwroot/css文件。 由于删除掉了css文件夹,页面元素肯定没有样式了。那么就简单的改造下默认的几个页面! 改造默认页面 index.razor 打开Pages/Index.razor,将演示组件SurveyPrompt 删掉。顺便把Shared/SurveyPrompt.razor也删除掉。将<h1>Hello, world!</h1> 替换为Ant Design组件。 @page "/" <Title Level="1">Hello,DotNet宝藏库</Title> <br /> <Text Type="success">欢迎关注我的公众号!</Text> Counter.razor 打开 Pages/Counter.razor,将代码改为如下: @page "/counter" <Title Level="2">HCounter</Title> <Divider /> <p role="status">Current count: @currentCount</p> <Button @onclick="IncrementCount" Type="primary">AntDesign 按钮</Button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } } FetchData.razor 打开Pages/FetchData.razor,将数据表格替换为Ant Design,删除页面所有代码,替换为Ant Design的示例! @page "/fetchdata" @using System.ComponentModel @using AntDesign.TableModels @using System.Text.Json @using MauiBlazorApp.Data @inject WeatherForecastService ForecastService <Table @ref="table" TItem="WeatherForecast" DataSource="@forecasts" Total="_total" @bind-PageIndex="_pageIndex" @bind-PageSize="_pageSize" @bind-SelectedRows="selectedRows" OnChange="OnChange"> <Selection Key="@(context.Id.ToString())" /> <PropertyColumn Property="c=>c.Id" Sortable /> <PropertyColumn Property="c=>c.Date" Format="yyyy-MM-dd" Sortable /> <PropertyColumn Property="c=>c.TemperatureC" Sortable /> <PropertyColumn Title="Temp. (F)" Property="c=>c.TemperatureF" /> <PropertyColumn Title="Hot" Property="c=>c.Hot"> <Switch @bind-Value="@context.Hot"></Switch> </PropertyColumn> <PropertyColumn Property="c=>c.Summary" Sortable /> <ActionColumn> <Space> <SpaceItem><Button Danger OnClick="()=>Delete(context.Id)">Delete</Button></SpaceItem> </Space> </ActionColumn> </Table> <br /> <p>PageIndex: @_pageIndex | PageSize: @_pageSize | Total: @_total</p> <br /> <h5>selections:</h5> @if (selectedRows != null && selectedRows.Any()) { <Button Danger Size="small" OnClick="@(e => )">Clear</Button> @foreach (var selected in selectedRows) { <Tag @key="selected.Id" Closable OnClose="e=>RemoveSelection(selected.Id)">@selected.Id - @selected.Summary</Tag> } } <Button Type="@ButtonType.Primary" OnClick="()=> { _pageIndex--; }">Previous page</Button> <Button Type="@ButtonType.Primary" OnClick="()=> { _pageIndex++; }">Next Page</Button> @code { private WeatherForecast[] forecasts; IEnumerable<WeatherForecast> selectedRows; ITable table; int _pageIndex = 1; int _pageSize = 10; int _total = 0; protected override async Task OnInitializedAsync() { forecasts = await GetForecastAsync(1, 50); _total = 50; } public class WeatherForecast { public int Id { get; set; } [DisplayName("Date")] public DateTime? Date { get; set; } [DisplayName("Temp. (C)")] public int TemperatureC { get; set; } [DisplayName("Summary")] public string Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public bool Hot { get; set; } } private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; public void OnChange(QueryModel<WeatherForecast> queryModel) { Console.WriteLine(JsonSerializer.Serialize(queryModel)); } public Task<WeatherForecast[]> GetForecastAsync(int pageIndex, int pageSize) { var rng = new Random(); return Task.FromResult(Enumerable.Range((pageIndex - 1) * pageSize + 1, pageSize).Select(index => { var temperatureC = rng.Next(-20, 55); return new WeatherForecast { Id = index, Date = DateTime.Now.AddDays(index), TemperatureC = temperatureC, Summary = Summaries[rng.Next(Summaries.Length)], Hot = temperatureC > 30, }; }).ToArray()); } public void RemoveSelection(int id) { var selected = selectedRows.Where(x => x.Id != id); selectedRows = selected; } private void Delete(int id) { forecasts = forecasts.Where(x => x.Id != id).ToArray(); _total = forecasts.Length; } } 运行效果: 总结 暂无,下次再会 欢迎大家关注我的微信公众号,一起进步,一起成长
2024-01-19
270
0
0
网络聚合
2024-01-19
Rust-01 启航
安装 所谓工欲善其事必先利其器,我们学习Rust当然需要安装Rust。我们可以从Rust官网下载rustup工具进行rust的安装。安装完成后,我们在命令行中输入rustc --version便可以查看我们所安装rust的版本。 笔者推荐在Windows平台使用PowerShell Core和Windows Terminal Rust相关命令 安装好 rust 后,我们需要了解几个东西 rustc rustc是 rust 的编译器,负责将 rs 文件源码编译到可运行文件或者库的二进制代码 rustup rustup是 rust 的升级管理工具,负责升级 rust 的版本,常用命令rustup update用来升级 rust cargo 一般都不直接使用rustc来直接编译 rs 文件,而是选择cargo。 cargo是 Rust 的包管理器,可以用来创建项目、安装依赖、类型检测、编译、运行以及测试项目等功能。cargo是一个功能强大的工具。cargo run能够自动化调用rustc` 对 rs 文件进行编译产出二进制文件并运行二进制文件。 常用cargo命令 命令 说明 示例 cargo new 创建项目,默认创建 binary 项目,添加参数可以选择 lib 和 bin 两种项目 cargo new hello --lib(默认--bin) cargo check 类型检测 cargo check cargo build 编译项目 cargo build [--release] cargo run 运行项目,可以带参数 cargo run [--release] cargo test 对项目进行单元测试 cargo test 详细的说明可以查看官方文档。 IDE 现在能够开发 rust 的工具已经有很多了,比如 Jetbrains 家的Clion(需要添加 rust 插件)或者 Microsoft家的VS Code(需要安装rust-analyzer扩展) Hello World! 既然已经有了 rust 的运行环境,那么我们便开始创建第一个 rust 项目。使用cargo new hello-world命令创建一个名为hello-world的项目,然后cd hello-world进入项目目录后可以看到项目的结构 C:. │ .gitignore │ Cargo.toml │ └─src main.rs 项目根路径会有一个Cargo.toml和一个src\main.rs,其中Cargo.toml文件是负责配置 cargo 和项目依赖项,main.rs 文件则是程序的入口点,main.rs 里的代码 fn main() { println!("Hello, world!"); } 简单易懂,关键字fn用来声明这是一个无返回值名称为main的 function,然后函数体内调用 rust 的输出宏println!输出了Hello, world! 在 Rust 中所有的函数调用都是必须有返回值的表达式,无返回值的返回一个空的 tuple 表示或者省略不写。 fn foo() -> () {} // 二者等效 fn foo() {} 我们运行cargo build,对项目进行编译,默认情况下是生成带有 debug 信息并且没有优化的代码,可以得到下图所示内容 而添加--release参数后,则会生成不带 debug 信息且优化后的代码,如下图所示(这一般是在正式发布时使用) 执行cargo build命令后会在项目目录下生成一个 target 文件夹,target 文件夹中的内容就是编译生成的结果。根据cargo build后面添加的参数会生成两个文件夹,即release和debug。 也可以直接运行cargo run命令直接运行项目,这个命令就相当于cargo build && ./debug/hello-world.exe先编译项目然后再执行编译后的可执行文件。和cargo build一样默认是生成 debug 代码,带上--release参数后则是 release 代码。 下图是cargo run所显示信息 下图是cargo run --release所显示信息 依赖项 在项目文件夹中找到Cargo.toml文件,这个文件便是 Rust 项目的依赖项配置文件 在这个文件里对项目进行配置,比如我现在需要使用随机数,那么我只需要在[dependencies]下面添加rand = "0.8.3",然后项目运行时,cargo 会自己进行依赖还原,将详细的包写入Cargo.lock文件中并且会自动去解析引入包的依赖,这和 npm 相似。 猜数字游戏 我们现在可以写一个小游戏来走一遍 Rust 项目创建到运行的流程 使用cargo new创建一个新的项目,然后在Cargo.toml文件中的[dependencies]下面添加rand = "0.8.3" 打开main.rs,并添加以下代码 use rand::Rng; use std::cmp::Ordering; use std::io; fn guess() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..101); loop { println!("Please input your guess."); let mut buffer = String::new(); io::stdin() .read_line(&mut buffer) .expect("Failed to read line!"); let number: i32 = match buffer.trim().parse() { Ok(num) => num, Err(_) => continue, }; match number.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } } fn main() { guess(); } 这些代码也很容易理解。use std::io;、use std::cmp::Ordering;和use rand::Rng;分别引入我们需要的模块。 std::io用来获取输入的数字,std::cmp::Ordering用来比较输入的数字和随机数的大小,rand::Rng用来生成随机数。 然后我们声明了一个无参数和无返回值的函数 guess。函数里主要是输出一行提示用户输入的提示信息和生成范围在[1,101)的随机整数,并根据用户输入的数字与随机数进行比较,直到用户猜测数字等于随机数字后结束程序。 我们需要处理用户的输入,在 rust 中使用io::stdin().read_line()从标准输入流中获取用户的输入,因为read_line()返回的是Result<usize>,在 rust 中Result<T>都可以使用 match(模式匹配)来对结果进行处理。loop在 rust 中是开启一个无线循环,根据内部的 break 来跳出循环,这里我们根据用户输入的数字和产生的随机数字比较结果作为是否结束循环的条件。在 rust 中,我们需要着重的学习match模式匹配,这里number.cmp返回一个Ordering,我们可以根据不同的结果进行不同处理,这和 if 条件判断类似,但是代码的可读性变高了,更利于理解,在 rust 中尽量使用模式匹配来进行逻辑判断以便减少 bug。main函数就是简单地调用guess函数。 使用cargo run后,我们会得到以下信息 总结 至此,我们了解了 rust 的安装和各种命令以及创建并运行一个 rust 项目的所有流程。 引用 部分内容和代码参考Rust官网
2024-01-19
177
0
0
网络聚合
65
66
67
68
69