[{"content":"","date":"17 April 2026","externalUrl":null,"permalink":"/","section":"Eric's Blog","summary":"","title":"Eric's Blog","type":"page"},{"content":"","date":"17 April 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":" 背景 # 我需要替换安卓手机的art组件来进行性能测试，以支持art开发。但是由于我的手机是user版本，即使通过magisk替换art组件，apexd仍然由于版本验证和哈希验证等原因拒绝使用我的art。尝试了各种方案，包括修改magisk脚本，更改art组件版本号等方式后仍未有效果，因此我转向重刷机成userdebug版本。\n过程 # 准备镜像 # 首先是官方的android16根本不提供userdebug版本，并且由于android16后的aosp不提供设备目录树，且pixel10之后的设备驱动不开放，所以尝试自己编译也是困难的。 最后我从社区找到了编译完成的android16 userdebug gsi镜像，选择最接近我原本版本的下载到服务器上。\n除了要刷进去的版本的镜像，还必须准备手机原厂镜像。从官网下载即可\n解压userdebug img.gz文件，得到img文件。\n解压原厂镜像，得到vbmeta.img\n刷入镜像 # 这里比较容易踩坑。\n首先澄清一点\nadb reboot bootloader 可以进入手机的fastboot模式\n而\nadb reboot fastboot 可以进入手机的fastbootd模式\n这两个模式不应该混用。\n首先\nadb reboot bootloader 进入fastboot模式\n然后\nfastboot --disable-verification flash vbmeta vbmeta.img 刷入原厂vbmeta.img\n然后\nfastboot reboot fastboot 进入用户空间\n然后删除原system分区\nfastboot erase system 再删除product分区(刷入gsi时需要，刷入原厂系统不需要)\nfastboot delete-logical-partition product_b 这里有可能显示找不到delete-logical-partition命令，那就说明fastboot版本太老\n鉴于我没有sudo权限，不能使用apt升级，我直接在aosp源码中make fastboot。然后使用编译的新版fastboot工具\n再刷入系统镜像\nfastboot flash system system.img 这个时候出现错误，发现无法刷入。\n经检查发现是社区编译的镜像文件不是simg(sparse image)格式，同样由于没有sudo安装比较复杂，我直接通过aosp编译获得img2simg工具\nmake img2simg-host 使用该工具获得simg镜像\n然后重新尝试指令刷入\n刷入完毕后通过fastboot进入recovery\nfastboot reboot recovery 必须在recovery界面手动格式化data分区\n根据知乎上的教程的说法，在新版安卓使用fastboot -w会破坏super分区，导致刷机失败\n重启后刷机成功。\n提示 # 如果在刷入system镜像时卡住，长时间无反应，可能是忘记重启进入fastbootd模式。在新版安卓中，必须在fastbootd模式下刷写system等动态分区。\n","date":"17 April 2026","externalUrl":null,"permalink":"/posts/1776426406276-post22/","section":"Posts","summary":"","title":"使用fastboot安装android16 qpr1 userdebug gis系统踩坑记录","type":"posts"},{"content":"之前在服务器的pixel手机进行profiling遇到了一些问题，主要体现为WIFI图标显示小叹号，知乎，小红书，抖音等app无法正确加载内容，显示联网错误，且登录显示时钟不正确。\n故而从网上寻求答案，通过他人博客获取解法\n在飞行模式执行\nadb shell settings delete global captive_portal_https_url adb shell settings delete global captive_portal_http_url adb shell settings put global captive_portal_http_url http://connect.rom.miui.com/generate_204 adb shell settings put global captive_portal_https_url https://connect.rom.miui.com/generate_204 修复WIFI叹号问题\n执行\nadb shell \u0026#34;settings put global ntp_server pool.ntp.org\u0026#34; 然后重启 修复时钟不同步问题\n结果是知乎和小红书可以登录，抖音可以正确加载视频，但可能由于刷过过多脚本，依旧无法登录\n","date":"13 April 2026","externalUrl":null,"permalink":"/posts/1776087861032-post21/","section":"Posts","summary":"","title":"关于pixel手机网络与时钟同步问题","type":"posts"},{"content":"","date":"16 March 2026","externalUrl":null,"permalink":"/tags/example/","section":"Tags","summary":"","title":"Example","type":"tags"},{"content":" Garbage-First Garbage Collection # 论文汇报 | 2026年3月 # 服务器风格GC 针对多核大内存机器， 满足软即时性和高吞吐目标。 利用并发避免过长停顿 目录 # 问题背景与概述 数据结构 核心机制1：驱逐暂停 核心机制2：并发标记 启发式方法 部分benchmark 总结 问题背景与概述 # 背景 # Java被广泛用来做大型服务器应用。 这些应用有着以下特点： 大量存活堆数据 相当量线程级并发 在高端多处理器机器上运行\n对这样的软件吞吐量很重要，同时他们还有严苛的（尽管是弹性的）即时性约束。\n$$吞吐量=用户代码执行的时间/(用户代码执行的时间+GC时间)$$ 问题 # Java语言通过GC来收割垃圾内存。 传统stop-world GC实现会影响应用反应速度， 而并发式或增量式GC中，更短的停顿时间又通常伴随着吞吐量的代价\n问题 # G1GC允许用户指定一个软即时目标，期望GC在任意y ms的时间片中消耗不超过x ms的时间。 通过显式声明目标，GC可以尽力保证GC停顿时间短和频率小来满足应用的要求，同时又不导致降低吞吐或增加不必要的负担。\nG1GC尝试在大堆，高分配率，运行在多核机器的应用上满足这样的一个软即时目标，同时维持高吞吐。\nG1GC的数据结构 # G1将堆划分成一组等大的heap regions，并会记录下region间双向的引用关系，这使得G1GC可以选择任何一个区域进行收割\n记录引用关系的方式是对于任一个region，它维护一个它的remembered set，RS会记录来自其它所有region（分代法中的young regions除外）指向改region的指针（的大约位置）。\n变异器线程会创建log records，一个并发线程处理这些log records来更新RS。\nG1GC的并发标记 # G1GC的并发标记算法使用SATB策略，提供可靠的对象存活性数据和每个区域的存活数据量分析。这为选择收割有更少的存活数据和更少的收割成本的region提供信息。\nG1GC的即时性 # G1GC采取粒度为region的可打断性，不保证硬即时，但比粒度为拷贝一个对象的硬性即使GC有更好的吞吐和空间利用，这对于大部分软件来说是好的tradeoff\n数据结构 # 堆 # heap region：G1堆被分成等大的regions，每一个region都是连续的虚拟内存空间。\ncurrent allocation region：正在分配空间的region。\nTLAB(thread-local allocation buffers)：变异器线程在堆区域上通过CAS预先分配的一块私有空间，后面变异器线程在自己的TLAB上分配对象，避免了竞争。\n空region被组织进链表，当前region满了以后可以在常数时间获得下一个region。\n超过region大小3/4的对象，会被放进专门的区域\nRemembered Set # remembered set(RS)：每个region有一个RS,指示所有region外可能有指针指向region内活对象的位置。\ncard table: 堆里的每512字节空间叫一个卡片。每个卡片被映射到卡表里1字节的卡项。\nremembered set log（RS log）：每个线程局部的脏卡片缓冲区\nfilled RS buffers：全局脏卡片的集合。\nRS本身是卡片的哈希表，由于并发，每个region有一组RS，每个线程一个哈希表，来允许各线程不互相干涉的更新RS。\nRS的逻辑内容是每个region的一组RS的并。\n1| rTmp := rX XOR rY 2| rTmp := rTmp \u0026gt;\u0026gt; LogOfHeapRegionSize 3| // Below is a conditional move instr 4| rTmp := (rY == NULL) then 0 else rTmp 5| if (rTmp == 0) goto filtered 6| call rs_enqueue(rX) 7| filtered: 在指针写后会触发RS写屏障\nRS写屏障的目的是对x.f = y，若x,y不属于同一region(1，2行)且y != null(4，5行)，并发GC线程就把x相应的卡片dirty掉（如果未dirty），然后指向这张卡片的指针放入该线程RS log中。\n当线程的RS log填满，整个RS log buffer会被加入到全局的filled RS buffer，然后线程创建一个新的RS log buffer。\n并发RS线程发现filled RS buffer大小超过初始阈值，就开始处理filled buffer，直到filled buffer大小小于初始阈值的1/4。处理的过程是，对每一个log buffer，一张一张处理里面的卡表项。而对每一张卡表项，先将其设为clean，以便在处理期间它被re-dirty，然后检查卡片内所有对象的指针域，如果发现其中有指向其他region的指针，就把卡片插入到被指向region的RS中。\n热卡片处理：一些卡片范围内的指针域频繁写入其他区域对象地址，把他们称作热卡片。我们希望识别热卡片并避免反复处理他们。我们使用另一张卡片哈希表记录卡片自上次驱逐暂停后的dirty次数，当次数超过阈值，就把它加入热队列，其中的卡片不会被并发期间处理，而是延迟到下次驱逐暂停的开始被统一处理。如果热队列满了，就选一个最不热门的卡片evict出去。\n当RS buffer增长过快，并发线程跟不上，超过一定大小时，尝试继续添加buffer的变异器线程需要自己处理RS。\n核心机制1：驱逐暂停 # collection set：我们选择要在驱逐暂停阶段清理的区域。\n简述： 在一些时机（见启发式部分）进行驱逐暂停。 这一阶段，驱逐collection set包含的区域，将这些区域的活对象驱逐到其他区域，然后释放这些区域。 我们选择尽可能并发处理这一阶段。 第一步是串行地选择collection set（见启发式部分）\n然后，多个GC线程并发地做三件事： 扫描待处理的RS buffer来更新RS， 扫描RS和其它root groups中的活对象， 驱逐活对象。\n为了达到更快的并行分配，使用GCLAB，线程分配一个需要驱逐的对象的拷贝到它的GCLAB中，然后在老对象位置安装一个转发指针。成功安装转发指针的线程负责拷贝和扫描内容并处理。\n使用基于工作窃取的技术提供负载均衡。\n分代GC # 当变异器线程分配region时，启发式地分配young regions。使得young regions一定被包含进下一次驱逐暂停的collection set。\n这样我们就不必处理young region指向其他region的RS log，而在驱逐暂停时直接处理其中的引用关系。\n除此之外young region和non-young region没有其他区别\nG1提供了分代模式和纯G1模式 分代模式有fully young和partially young两个模式， fully young只处理所有young regions， partially young同时处理所有young regions和一些专门选取的non-young regions。\n核心机制2：并发标记 # 并发标记提供了对象的存活性信息和region的存活数据量等统计信息。\nSATB: snapshot-at-the-beginning。 标记算法保证提供的信息是基于标记工作开始时的对象图情况。 标记期间新分配的对象被认为是存活的，不构成对象图的一部分。\n对于bitmap和TAMS，都维护previous和next两组信息。 previous是上次并发标记完成的结果信息，驱逐暂停等过程可以直接利用这些信息。 next是并发标记正在构建的信息。 他们会在标记结束时交换角色\n逻辑上 previous next 丢弃 previous next\n数据结构 # 使用bitmap来指示对象存活性信息，1bitmap比特可以映射64堆比特。 使用mark stack来放置灰对象（标记了但没有扫描完）。\n每个堆维护TAMS(top at mark start)，指示当前标记过程的范围， TAMS以上的对象是标记期间分配的对象，认为它们被隐式标记了。\n初始标记暂停 # 先并发地清空next bitmap。 停止所有变异器线程 初始标记暂停开始： 标记所有从根直达的对象。 遍历所有堆区域，令每个区域 $nextTAMS=区域top$。\n变异器线程重新启动\n并发标记开始：\n堆遍历的优化：Finger遍历bitmap里被标记的bit，高于finger的对象是灰色，低于finger的会对象被放进标记栈\n并发标记写屏障 # 变异器有可能在正在并发标记时更改指针图，破坏SATB保证。因此在指针写之前添加并发标记写屏障\n1| rTmp := load(rThread + MarkingInProgressOffset) 2| if (!rTmp) goto filtered 3| rTmp := load(rX + FieldOffset) 4| if (rTmp == null) goto filtered 5| call satb_enqueue(rTmp) 6| filtered: 当进行 \\[rX,FieldOffset\\]=rY 如果在并发标记阶段，且rX+FieldOffset位置不为null，就把rX+FieldOffset的值加入到当前线程的标记缓冲区，当标记缓冲区满，就被加入到全局标记缓冲区，然后并发标记线程定期检查集合大小，打断堆遍历过程来处理填满的缓冲区。\n最终标记暂停 # 停止所有变异器线程，多线程地处理所有的未被处理的全局标记缓冲区里的log和线程局部缓冲区里的log。\n存活数据计数和清理 # 当最终标记暂停完成时，存活数据计数会并发地进行，GC线程重新检查每个region，通过bitmap计数区域内的被标记数据的量。\n并发标记期间发生的驱逐暂停会增加next TAMS的值，cleanup暂停可靠的终止计数过程。 这一阶段next和previous bitmap和TAMS交换身份\n最后cleanup阶段按照GC效益给各个regions排序\n不包含存活数据的区域会在这个阶段立刻被清理。\n驱逐暂停和标记的交互 # 驱逐暂停不会回收标记阶段标记的死对象\n驱逐一个对象时，必须保证标记正确，这个对象在previous marking中是活的，且必须让next marking知道其新的位置变更。\n必须允许在并发标记进行时发生驱逐暂停，为了做到这一点，把标记栈作为根的来源之一。\n热门对象处理 # 热门对象：被很多其他位置引用的对象\n在堆区域中预留一个前缀区专门放热门对象，尝试快速地识别热门对象，然后把它们放在前缀区。而前缀区的这些regions不会被回收。\nRS大小达到给定阈值的regions触发热门暂停，热门暂停在region的每个对象上安放大约的引用计数，引用计数快速达到阈值的对象会被单独驱逐到前缀区，非流行对象驱逐到堆的普通部分。如果没有发现热门对象，就没有驱逐发生，但区域的阈值翻倍。\n修改RS写屏障\nif (rY \u0026lt; PopObjBoundary) goto filtered 这样，若rY为null或rY为热门区对象，指向它的卡片将不被记录进log\n启发式方法 # 用户输入：\n内存空间使用的上界 软即时目标：在每个x ms时间片中，GC只能消耗不超过y ms的GC时间 是否使用分代模式 实现软即时目标 # 保证单次暂停不超过暂停时间的上界 调度暂停防止短时间内过多次暂停 预测暂停时间 # 对fully-young模式，我们分析一次驱逐暂停多少young regions能产生想要的时间间隔。两次驱逐暂停期间能分配多少young regions。 而partially-young模式，如果暂停时间允许的话，可能添加更多non-young区域加入收割，在这个模式下和纯G1模式下， 如果即使选择最好的区域也会超过暂停时间界限，我们就停止选择区域。\n对于后者 $$V(cs)=V_{fixed} + U * d + \\Sigma_{r\\sub cs} (S · rsSize(r) + C · liveBytes(r))$$\\( V(cs)\\) is the cost of collecting collection set cs; \\(V_{fixed}\\) represents fixed costs, common to all pauses; \\(U\\) is the average cost of scanning a card, and \\(d\\) is the number of dirty cards that must be scanned to bring remembered sets up-to-date; \\(S\\) is the of scanning a card from a remembered set for pointers into the collection set, and \\(rsSize(r)\\) is the number of card entries in r\u0026rsquo;s remembered set; and \\(C\\) is the cost per byte of evacuating (and scanning) a live object, and \\(liveBytes(r)\\) is an estimate of the number of live bytes in region r.\n\\(V_{fixed}\\),\\(U\\) ,\\(S\\),\\(C\\)，是比较恒定的，我们一开始采用一个初始估计，然后在实际运行时动态修正。\n引入confidence置信值，以给定标准差的倍数调整参数。\n剩下的几个变量是在驱逐暂停开始时计算出来的， 对于liveBytes来说如果一个区域包含的对象上次在并发标记开始时就分配好了。上次并发标记就提供了一个存活数据的上界。 而对于上次标记之后新分配的区域，可以动态估计存活率，然后用存活率计算预期的存活数据量。还可以跟踪存活率方差，根据置信值调整。\n调度暂停 # 只要有充足的内存空间，我们就可以延迟任何标记暂停和驱逐暂停。\n当年轻代的驱逐暂停不得不被推迟时，我们可以让变异器线程把对象分配到老年代区域。\n使用一个start/stop时间对队列，处理最近发生的暂停时间片，从一段插入暂停，从另一端丢弃已经离开时间片的无关时间对。 我们可以知道现在最多能暂停多久，以及多久之后我们才能暂停一个已知时间长度的暂停。\n选择collection set # $$预期GC效率 = 估计的垃圾量/估计的回收成本$$在标记结束时我们获得了初步的效益估计以及排序，但是这会随时间改变，所以在驱逐暂停开始时，我们选取初步排序中固定大小的前缀依据当前的效益估计进行重新排序。然后选择区域，当时间超过或空间不够时停止选择。\n何时发起驱逐暂停 # 选择一个硬边际变量h，假设堆大小为M， 有硬极限$H = (1-h)*M$，硬极限就是我们至少要保证堆中有H的空间，否则进行驱逐时会发生没有空间驱逐的问题。当分配的空间到达了应边界，必须发起驱逐暂停。\n在fully-young模式下，维护一个关于有多少个年轻代region时进行驱逐暂停能满足暂停时间的动态估计，当年轻代区域数量达到估计时发起驱逐暂停。 在partially-young模式下，只要软即时目标允许，我们就发起驱逐暂停。\n分代模式一开始是fully-young，一个并发标记之后转向partially-young模式，然后当partially-young模式效益降低到fully-young一致时转回fully-young模式。但是当堆快满时，即使效益降低，也必须进行partially-young来降低堆占用\n何时发起并发标记 # 维护一个软边界u 定义软极限$ H - uM = M - hM - uM $ 在驱逐暂停前，当堆占用达到软极限，只要软即时允许，标记就会被发起。\n总结与未来工作 # G1 的三大核心贡献 # 避免碎片化 首个高性能服务器收集器，通过充分压缩避免细粒度空闲列表 简化内存管理，基本消除碎片问题 智能选区策略 基于全局标记信息和 GC 效率公式（垃圾量/成本）优先选择区域 而非仅依据存活数据量，实现暂停时间目标 软实时调度 使用滑动窗口队列跟踪历史暂停，调度未来 GC 比硬实时更灵活，允许时间片内多次不同长度的暂停 未来优化方向 # 写屏障优化：改进 remembered set 表示，提高处理效率 静态分析：探索在编译期移除部分写屏障的可能性 逃逸分析：结合编译时逃逸分析，进一步优化对象分配 ","date":"16 March 2026","externalUrl":null,"permalink":"/posts/1773664352206-post20/","section":"Posts","summary":"","title":"Garbage-First Garbage Collection","type":"posts"},{"content":"","date":"16 March 2026","externalUrl":null,"permalink":"/tags/tag/","section":"Tags","summary":"","title":"Tag","type":"tags"},{"content":"","date":"16 March 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":" 例子(抖音) # 使用dumpsys meminfo com.corporation.package可以查看应用使用内存的情况\nblazer:/ $ dumpsys meminfo com.ss.android.ugc.aweme Applications Memory Usage (in Kilobytes): Uptime: 1954149922 Realtime: 2520666124 ** MEMINFO in pid 24989 [com.ss.android.ugc.aweme] ** Pss Private Private Swap Rss Heap Heap Heap Total Dirty Clean Dirty Total Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 96260 96244 0 0 100616 132796 104822 23789 Dalvik Heap 142377 142324 0 0 150532 224107 125803 98304 Dalvik Other 21484 21332 0 0 23204 Stack 8864 8864 0 0 8872 Ashmem 1172 160 0 0 3984 Other dev 144 0 144 0 456 .so mmap 85092 6820 72432 0 165076 .jar mmap 1694 0 4 0 51284 .apk mmap 47703 500 38372 0 69472 .ttf mmap 1112 0 688 0 1808 .dex mmap 560981 772 558064 0 564252 .oat mmap 318 0 0 0 13212 .art mmap 1846 1328 368 0 22832 Other mmap 25399 52 22748 0 35620 EGL mtrack 51460 51460 0 0 51460 GL mtrack 22580 22580 0 0 22580 Unknown 76309 74628 1680 0 77792 TOTAL 1144795 427064 694500 0 1363052 356903 230625 122093 App Summary Pss(KB) Rss(KB) ------ ------ Java Heap: 144020 173364 Native Heap: 96244 100616 Code: 677652 865404 Stack: 8864 8872 Graphics: 74040 74040 Private Other: 120744 System: 23231 Unknown: 140756 TOTAL PSS: 1144795 TOTAL RSS: 1363052 TOTAL SWAP (KB): 0 Objects Views: 2883 ViewRootImpl: 1 AppContexts: 9 Activities: 1 Assets: 32 AssetManagers: 0 Local Binders: 196 Proxy Binders: 116 Parcel memory: 132 Parcel count: 474 Death Recipients: 5 WebViews: 1 Native Allocations Count Total(kB) ------ ------ Bitmap (malloced): 131 4575 Other (malloced): 7725 737 Other (nonmalloced): 342 200 SQL MEMORY_USED: 6285 PAGECACHE_OVERFLOW: 4686 MALLOC_SIZE: 3563 DATABASES pgsz dbsz Lookaside(b) cache hits cache misses cache size Dbname PER CONNECTION STATS 4 20 37 1 150 2 /data/user/0/com.ss.android.ugc.aweme/databases/Content.db 4 32 43 2 245 4 /data/user/0/com.ss.android.ugc.aweme/databases/free_flow 4 8 0 0 0 (attached) temp 4 32 27 1 146 2 /data/user/0/com.ss.android.ugc.aweme/databases/free_flow (2) 4 48 115 8798 236 17 /data/user/0/com.ss.android.ugc.aweme/databases/ss_app_log.db 4 48 110 181 167 10 /data/user/0/com.ss.android.ugc.aweme/databases/ss_app_log.db (2) 4 28 23 1 150 2 /data/user/0/com.ss.android.ugc.aweme/databases/aweme.db 4 20 23 1 160 2 /data/user/0/com.ss.android.ugc.aweme/databases/verifystorage.db 4 20 29 2332 161 3 /data/user/0/com.ss.android.ugc.aweme/databases/applog_priority_db_P1@1128 4 20 29 1 162 2 /data/user/0/com.ss.android.ugc.aweme/databases/TTVideoEngine_vod_strategy_database_v01 4 44 123 1384 157 7 /data/user/0/com.ss.android.ugc.aweme/databases/downloader.db 4 48 67 133 150 4 /data/user/0/com.ss.android.ugc.aweme/databases/bytebench_db 4 20 29 2326 161 3 /data/user/0/com.ss.android.ugc.aweme/databases/applog_priority_db_P0@1128 4 20 31 1 148 2 /data/user/0/com.ss.android.ugc.aweme/databases/OriginalSound 4 64 23 0 261 2 /data/user/0/com.ss.android.ugc.aweme/databases/pz_database 4 8 0 0 0 (attached) temp 4 64 25 4 152 2 /data/user/0/com.ss.android.ugc.aweme/databases/pz_database (3) 4 64 45 30 162 5 /data/user/0/com.ss.android.ugc.aweme/databases/pz_database (2) 4 44 116 58 155 9 /data/user/0/com.ss.android.ugc.aweme/databases/effect_platform_effect_db 4 60 23 0 239 2 /data/user/0/com.ss.android.ugc.aweme/databases/bd_sync_sdk_v4.db 4 8 0 0 0 (attached) temp 4 60 81 76 147 3 /data/user/0/com.ss.android.ugc.aweme/databases/bd_sync_sdk_v4.db (2) 4 24 41 5 269 4 /data/user/0/com.ss.android.ugc.aweme/databases/deviceinfo_stats 4 8 0 0 0 (attached) temp 4 24 29 3 158 2 /data/user/0/com.ss.android.ugc.aweme/databases/deviceinfo_stats (2) 4 4168 62 1976 163 5 /data/user/0/com.ss.android.ugc.aweme/databases/applog_priority_db_P2@1128 4 20 25 1 160 2 /data/user/0/com.ss.android.ugc.aweme/databases/DOUYIN_SCORE.db 4 20 47 88 165 5 /data/user/0/com.ss.android.ugc.aweme/databases/video_record.db POOL STATS cache hits cache misses cache size Dbname 1 151 152 /data/user/0/com.ss.android.ugc.aweme/databases/Content.db 5 411 416 /data/user/0/com.ss.android.ugc.aweme/databases/free_flow 8979 416 9395 /data/user/0/com.ss.android.ugc.aweme/databases/ss_app_log.db 1 151 152 /data/user/0/com.ss.android.ugc.aweme/databases/aweme.db 1 161 162 /data/user/0/com.ss.android.ugc.aweme/databases/verifystorage.db 2332 162 2494 /data/user/0/com.ss.android.ugc.aweme/databases/applog_priority_db_P1@1128 1 163 164 /data/user/0/com.ss.android.ugc.aweme/databases/TTVideoEngine_vod_strategy_database_v01 1384 158 1542 /data/user/0/com.ss.android.ugc.aweme/databases/downloader.db 133 151 284 /data/user/0/com.ss.android.ugc.aweme/databases/bytebench_db 2326 162 2488 /data/user/0/com.ss.android.ugc.aweme/databases/applog_priority_db_P0@1128 1 149 150 /data/user/0/com.ss.android.ugc.aweme/databases/OriginalSound 25 591 616 /data/user/0/com.ss.android.ugc.aweme/databases/pz_database 58 156 214 /data/user/0/com.ss.android.ugc.aweme/databases/effect_platform_effect_db 78 406 484 /data/user/0/com.ss.android.ugc.aweme/databases/bd_sync_sdk_v4.db 10 447 457 /data/user/0/com.ss.android.ugc.aweme/databases/deviceinfo_stats 1976 164 2140 /data/user/0/com.ss.android.ugc.aweme/databases/applog_priority_db_P2@1128 1 161 162 /data/user/0/com.ss.android.ugc.aweme/databases/DOUYIN_SCORE.db 88 166 254 /data/user/0/com.ss.android.ugc.aweme/databases/video_record.db 各个列指标解释 # Pss # Proportional Set Size (按比例计算的实际使用的物理内存)\n$$ Pss = \\Sigma(每个页 * 1/共享该页的进程数)*页面大小 = (所有独占的页+与其他进程共享的页*对应的占比)*页面大小 $$实际上是用分摊的想法考虑共享的页的情况下计算出的物理页帧占用量，核心是共享的物理页帧按照占比计入\n意义 : 最接近物理内存的实际使用量 用于评估整体内存压力 Rss # Resident Set Size (按所有驻留页计算的使用的物理内存)\n$$Rss = \\Sigma(所有映射到物理页帧的页)*页面大小$$相比于Pss共享页按照100%计入\n意义 : 可以表示进程占用的物理内存的上限。 Private Dirty # $$Private Dirty = \\Sigma (仅被本进程映射的脏页)*页面大小$$ 意义 : 所有仅被当前进程映射的内容被修改过，必须写回swap才能回收的内存。 是GC的主要回收对象 Private Clean # $$Private Clean = \\Sigma(仅被本进程映射且未被修改过的页)*页面大小$$ 意义 : 进程独占但可以直接丢弃无需IO的内存 $$ PrivateDirty \u003c= Private Total \u003c= Pss \u003c= Rss$$ Swap Dirty # $$ Swap Dirty = \\Sigma(已被换出到zram/swap且内容被修改过的页)*页面大小$$ 意义 : 已释放的物理内存，重新访问需要从swap读回 HeapSize # 堆的最大可分配容量\nHeapAlloc # 堆的已分配给对象的内存\nHeapFree # 堆内空闲块的总和\n$$HeapSize = HeapAlloc + HeapFree$$Alloc/Size过高时将触发GC\n各个行指标解释 # Dalvik Heap # ART管理的Java对象堆\nDalvik Heap Pss = Σ(存活 Java 对象的内存) + Σ(GC 元数据: mark bits, card table) + Σ(分配器碎片) + Σ(Thread Local Allocation Buffers) Dalvik Heap Private Dirty = Java对象实际占用 Dalvik Heap Heap Alloc = ART分配器记录的已分配字节数 Dalvik Heap Heap Free = ART空闲链表中的可分配块 这里Private Dirty是内核物理值，Heap Alloc是ART的逻辑值。\nDalvik Other # 这部分内存由ART直接mmap或malloc分配，不经过GC堆分配器，GC不回收它们，其生命周期与进程一致。\n.art mmap/.dex mmap/.oat mmap # [.xxx map] 文件映射的内存区域\n映射类型: 内容来自磁盘文件 共享性: 写时复制，初始可共享 初始状态: 与文件保持一致 写后状态: Dirty+Private COW创建副本 .dex mmap Private Clean = 未被执行的 DEX 代码页（可共享） .dex mmap Private Dirty = 被 JIT 修改过的代码页（COW 后私有） .oat mmap Private Clean = AOT 编译的机器码（只读，可共享） Native Heap # $$Native Heap = JNI/Bitmap/图形等非GC管理的native内存$$ Ashmem/EGL mtrack/GL mtrack # 主要是Android 图形/共享内存子系统\n","date":"3 March 2026","externalUrl":null,"permalink":"/posts/1772541458610-post19/","section":"Posts","summary":"","title":"如何阅读adb dumpsys meminfo","type":"posts"},{"content":" mindmap root((Runtime Systems生态位图谱)) 编译型高性能 JVM Java/Scala/Kotlin 企业级应用 大数据处理 长期运行服务 Go Runtime 云原生基础设施 微服务 高并发网络服务 .NET CLR Windows生态 企业应用 游戏开发 解释型灵活开发 Python CPython AI/ML领域 快速原型 脚本自动化 Ruby MRI Web开发 快速迭代 开发者友好 PHP Zend Web应用 CMS系统 中小型网站 JavaScript生态 V8 Node.js 全栈开发 实时应用 前后端统一 Deno 安全沙箱 现代JS/TS 分布式工具 新兴语言运行时 Rust 无运行时 系统编程 WebAssembly 安全关键系统 Swift Runtime Apple生态 移动应用 服务端 函数式运行时 Erlang BEAM 电信系统 高可用系统 即时通讯 Elixir Web实时应用 并发系统 可扩展服务 基础与定义 # A run-time system is defined as a software component responsible for dynamic management of program execution and of the resources granted by the OS.\nIn other words, an RTS mediates interaction between the application and the OS.\nDifferent from run-time environment: A RTS is distinct from and embedded into a more broad runtime environment, which is defined to include the OS that is responsible for overall resource management, and RTS instances managing other running applications unaware of each other’s existence and resource demands\n是一个介于操作系统和上层应用之前的软件组件，其核心功能是为程序运行提供必要依赖支持，包含运行库的实例化调用及运行时系统的资源管理，贯穿程序生命周期中的执行阶段。\n运行时分类 # 1. 运行时库vs运行时虚拟机 # 运行时库：一组在运行阶段起关键作用的库，负责程序启动、初始化、标准库实现、与 OS 交互、异常处理、清理退出等。如printf,malloc/free以及真正的入口_start就来自运行时库。\n2. 托管运行时vs非托管运行时 # 托管运行时： 程序跑在一个“虚拟机 / 托管执行环境”里，运行时在中间“管”着你的代码，提供自动内存管理（GC）、类型安全检查、安全沙箱、异常处理等服务。典型例子：JVM、.NET CLR、JavaScript 的 V8 等。 非托管运行时系统：程序直接跑在操作系统和 CPU 上，运行时只是“帮手”，提供启动代码、标准库函数等，但不帮你“管”内存安全、类型安全这些事。典型例子：C/C++ 的 C 运行时库（CRT）、各种 native runtime。\n运行时系统的核心功能 # 软件生命周期中的运行时系统 # RTS最初是在程序运行时发挥作用，程序运行时包括启动，初始化，执行和终止阶段，包括潜在的动态链接，加载和JIT/AOT编译。\nC的最小RTS crts0是被连接器/加载器添加，然后初始化执行环境（栈/堆栈指针/寄存器/信号处理器），通过运行main函数把控制移交给程序，最终将main的返回值传递给运行时环境。\n由于crts0是平台特定的，并由汇编实现的，在不同架构上就需要重新编译，因此作为“抽象机器”的功能没有被提供。\n更精密的RTS要么被实现为一个独立的库，被链接进应用的object code来创建一个可执行程序，要么他们自身就是一个接受程序代码作为输入的应用程序，这个输入通常是解耦了符号编程语言和目标架构原生指令集的IR形式,之后会通过解释或JIT编译成原生代码。\n结构和功能 # RTS的主要功能是在基于特定执行模型实现的抽象机器上管理程序的执行，它要包括和OS交互，内存管理，线程和任务管理，通信和监视等\n执行模型： # 最常用的模型是stack machine，一个栈用来存放函数参数以及函数调用的中间结果。这个顺序模型和register machine（栈被一系列映射到机器寄存器的虚拟寄存器替代）通常被命令式和面向对象语言采用。\n还有一种有名的模型是graph reduction，通常被基于lambda演算的函数式语言使用，该模型适合并发，因为图的部分可以被独立规约。还有term-rewriting模型\u0026hellip;\nOS交互 # RTS依赖OS来进行资源分配，IO和网络以及提供一个掩盖操作系统细节同时提供架构独立的API给程序员。RTS通常直接实现高级语言的特性和难以在语言自身中直接实现的低级语言原语。\n错误和异常处理 # 通过全局errno变量来进行错误处理是很简陋的。更现代的语言通常提供自定义的信号处理和基于try/catch机制的异常处理，这通常导致复杂的控制流，精密的RTS可以在运行时透明的处理错误。\n内存管理 # 大多数高级语言需要RTS提供GC能力来自动处理堆对象内存的分配和回收。常见的GC策略有引用计数式和追踪式两大类。GC还会通过compacting周期性将已使用的对象聚集在一起来避免内存碎片化，通过generational基于“存活越久的对象越有可能继续存活，而大多数对象会在短时间内消亡”将长期存活的对象提升到回收频率低的堆区域。在多核架构上一个潜在的问题是Stop the world停顿影响性能。\nreference-counting:引用计数GC，通过对持有对象的引用进行简单计数，计数为0时释放，缺点，单独使用时会发生循环引用内存泄露。\n引用计数式:\npython = reference-counting + generationalGC # 在引用技术基础上通过分代GC去除循环引用：将对象分为多代，一- 开始新创建的对象都是第0代，当 $$分配数-释放数\u003e阈值$$对所有第0代对象执行一次循环GC，将经历过这次GC的对象提升到下一代，同时对部分老年代做GC。 GC算法：选出一批要要扫描的容器对象，给每一个对象一个临时计数，初始化为其引用计数数目，遍历所有容器，对每个容器遍历它引用的所有对象，将这些对象的临时计数-1，此时若一个容器对象计数\u0026gt;0，说明它被外部引用，它是可达的。若计数==0，说明只有集合内部的对象引用它，但是这个集合内部对象可达不可达未知，所以再遍历一次，从所有计数\u0026gt;0的对象出发，把所有到达的对象标记为计数设为1，此时计数仍未0的对象就是不可达循环引用环的成分。\n追踪式:\nGO # 早期Mark And Sweep:\nMark阶段从Roots往下逐渐标记，没有被标记到的就是无法被访问到的垃圾内存\nSweep阶段统一清除垃圾内存\n后期三色标记法:\n白色：尚未被访问\n灰色：被访问到，但是它引用的对象还没有被完全扫描\n黑色：被访问到，且它引用的对象被扫描完\nMark阶段：\n将根标记为灰色，循环取出灰色的节点A直至没有灰色节点，将A引用的节点标记为灰色，当标记完A所有节点时A变为黑色。\nSweep阶段：\n清除所有GC后依然为白色的节点。\n为了防止全程STW，引入写屏障来支持并发GC\n危险情况：\n黑色对象引用了白色对象。 灰色对象对白色对象的引用被删除。 并发时如果GC进行了一部分，同时有1和2发生，就会导致有正在使用的对象被当成垃圾内存回收掉。\n例如:\na, c are the roots.\na -\u0026gt; b -\u0026gt; s1, s2, s3;\nc -\u0026gt; d;\n如果b刚变为黑色，c把d引用传给了b，即b-\u0026gt;s1,s2,s3,d 此刻，b是黑色，d是白色，还没有GC到，如果没有其他事情发生，d会被通过c扫描到，然后变为灰色和黑色。 但是恰好此刻c断开对d的引用。那么d将无法被GC扫描到，GC结束时d是白色，会被错误清除。\n必须维护三色不变式：\n强三色不变式： 不允许任何黑色对象引用白色对象。 一旦满足，就绝不可能漏标白色对象。 弱三色不变式： 黑色对象可以引用白色对象，但这些白色对象必须被其它灰色对象引用（或存在一条从灰色对象到它的路径）。 只要“灰色 → 白色”的路径在，白色对象最终就会被扫描到。 两个不变式中任何一个成立就可以防止GC错误删除有用的对象。\nGO引入写屏障保障并发安全性\n插入写屏障: 若正在GC，当对象 A 增加一个指向 B 的引用时，把 B 标记为灰色。 删除写屏障： 若正在GC，当对象 A 断开对 B 的引用时，如果 B 是灰色或白色，就把 B 标记为灰色。 ART # ART将内存划分为多个空间\n混用多种GC算法：包括Mark Sweep，Semi Space，Mark Compact和分代法。\nSemi Space: 堆分为等大的From和To空间，GC时将From空间的存活对象复制到To空间。 To空间变为新From空间，原From空间清空\nMark Compact: 先做标记，再将存活对象向堆的一端移动，消除碎片，使空闲空间连续，然后遍历修正所有对象引用地址。\n线程和任务管理 # 操作系统级别的threads和RTS级别的tasks(lightweight threads)，是支持并发和并行的核心。RTS有时管理一个线程池和一个任务池，将一组任务多路复用到一组操作系统线程上来实现拓展性。许多语言提供显式线程编程模型，然而由于存在可观察的非确定性以及静态条件和死锁问题，这种模型被认为是一个糟糕的选择，因此诸如事务内存以及半显式或基于骨架的模型等更高级别的编程模型获得了越来越多的关注。\n显式的同步原语还引起了粒度的权衡，这对可移植性能有害：全局锁安全，但牺牲并行性；细粒度锁会导致难以承受的开销。执行协调，包括跨异构架构的调度和工作分配，显著增加了RTS决策复杂度。这就是可选任务/线程优于强制任务/线程的原因：RTS仅在判定值得时才会并发执行任务。\n两个主要的工作分发机制是work pushing，急切地卸载工作和work stealing，工作分配由需求驱动。\n通信 # 大规模的软件需要的可拓展性超出了单节点的范畴，这要求必须将工作分发到集群/网格/云端。两种常见的方案包括shared memory（可以是分布式，需要额外的抽象层）和message passing。如果通信跨越网络进行，则需要对计算和数据进行序列化。对于共享上下文信息的分布式协作RTS来说，发布/订阅等更复杂的协议可能更好。\n监控 # 性能分析，调试和执行重放依赖于追踪（即对相关事件的记录），而动态优化则依赖运行时通过监控获取的系统信息。常见的性能分析策略按开销递增依此为：累计汇总统计，普查式统计，细粒度基于时间统计。\nRuntime 生态概览 # 一、JVM：企业级与大数据的主流选择 # 核心场景：\n传统企业后端（银行、电商）、微服务架构、中间件。 大数据平台（Hadoop/Spark）及数据工程。 生态现状： 成熟稳定： 发展二十余年，工具链完善，配合 Spring 框架是企业级后端的事实标准。 云原生挑战与优化： 针对传统 JVM 启动慢、内存占用高的问题，目前通过 GraalVM Native Image 实现原生编译，以及厂商优化 JDK（如 Azul）来适应 Serverless 和容器场景。 定位： 坐拥“企业后端 + 大数据”基本盘，正向云原生场景延伸，但在轻量级高并发领域面临 Go/Rust 的竞争。 二、V8 / JavaScript 运行时：覆盖面最广的跨端生态 # 核心构成：\n浏览器引擎： V8（Chrome/Edge）、JavaScriptCore（Safari）等，垄断 Web 前端。 服务端与边缘： Node.js 存量巨大；Deno、Bun 等新一代运行时追求更快的启动速度与原生 TypeScript 支持；边缘运行时主要用于低延迟短任务。 生态现状： 前端地位不可撼动。 后端在 BFF、API 服务及工具链领域普及度高。 边缘计算与无服务器场景增长迅速。 定位： “前端统治 + 后端稳固 + 边缘扩展”，是目前覆盖场景最广泛的运行时生态。 三、CLR / .NET Runtime：微软系的跨平台企业运行时 # 核心场景：\nWindows 生态企业系统（ERP/CRM）、ASP.NET Core Web API。 桌面应用、游戏开发及云原生应用。 生态现状： 深度整合： 与 Visual Studio、Azure 云服务紧密绑定，开发体验好。 现代化演进： .NET 5+ 统一并实现了真正的跨平台。.NET 8 在性能、GC 和 AOT 编译上优化显著，已转型为现代云原生应用基础平台。 定位： 类似 JVM 的企业级运行时，深耕微软技术栈，正向跨平台与高性能云原生方向演进。 四、ART（Android Runtime）：移动端基础设施 # 核心场景： Android 应用执行（Java/Kotlin 编译为 dex/oat）。 生态现状：\nAndroid 系统的唯一官方运行时，深度绑定系统底层，管理应用沙箱、权限与内存。 属于操作系统级抽象，不具备通用性，但在 Android 生态内地位不可替代。 定位： 移动端应用执行的底层基石。 五、动态语言运行时：以开发效率与生态取胜 # 1. Python (CPython) # 场景： AI/数据科学、自动化脚本、Web 后端。 特点： 解释执行，性能虽不及 JVM/V8，但胜在科学计算库生态极其丰富。 优化： 通过 PyPy JIT 或 C/Rust 扩展弥补性能短板。 2. PHP (Zend Engine) # 场景： 传统 Web 后端、CMS（如 WordPress）。 特点： 部署简单，在内容类站点中存量巨大。 定位： 不追求极致性能，核心优势在于庞大的库生态与高效的开发效率，分别在 AI 领域和 Web 内容领域占据强势地位。 六、WebAssembly 运行时：新兴的跨语言基座 # 核心特性： 二进制指令格式，具备可移植、沙箱化、接近原生性能的特点。通过 WASI 扩展至非浏览器环境。 核心场景：\n浏览器侧： 复杂图形、音视频处理、AI 推理等高性能模块。 服务端/边缘： 作为安全沙箱，支持多语言模块混合运行，适用于 Serverless 与边缘计算。 定位： 快速成长的新兴生态。并非直接替代传统运行时，而是作为浏览器的高性能补充和服务端的多语言安全运行基座。 ","date":"14 February 2026","externalUrl":null,"permalink":"/posts/1771043013887-post18/","section":"Posts","summary":"","title":"运行时系统初步认识","type":"posts"},{"content":" 个人简介 # Eric是典型的理工男，母胎solo。性格INTJ，略微孤僻，混沌善良。\n爱好:编程，暹罗猫，雨天。\n核心技术栈 # 💻 编程语言\nC++ —— 系统级开发与高性能计算\n⌨️ 编辑器 \u0026amp; IDE\nNeovim（终端高效流）｜VS Code（轻量扩展强）｜Visual Studio（Windows/.NET 深度集成）\n🏗️ 构建系统\nCMake（跨平台配置）｜Make（经典自动化）\n🔥 编译器\nClang/LLVM（现代诊断 \u0026amp; 工具链）｜GCC（GNU 生态主力）\n🐞 调试工具\nGDB（Linux 标配）｜LLDB（LLVM 调试器，集成于 VS/Xcode）\n专业 # 编程语言\nEric擅长C++，掌握C++17，值类型，移动语义，完美转发，模板元编程，多线程，promise异步，对Qt，cocos2dx等框架有简单使用经验；了解语法并可以基本使用Python/C#/Java/Rust等。\n数理基础\nEric对离散数学，微积分，级数等数学理论掌握比较扎实，有较好的数理基础。\n计算机底层知识\nEric已经学习过CSAPP的主要内容和计算机组成与体系架构(COA)，掌握计算机的分层视角，对计算机中程序运行的计算机组成与操作系统的工作过程有较为深刻的理解。\n算法能力\nEric有基础的数据结构与算法能力。认识各种常见数据结构，了解并手写过并查集，二分查找，图查找等基础算法。但Eric的算法能力依旧相当基础，并不能从算法竞赛中脱颖而出，遂放弃。\n后端\nEric对后端开发有一定认识。Eric使用Java ssh传统技术栈开发过基于Tomcat的简单MVC后台系统，认识ORM，DI容器等开发后台需要的技术框架，也了解比较现代的RestFul API，掌握SOLID五原则与一些基本设计模式，了解MVVM，DDD等现代架构。\n前端\nEric对前端开发有一定认识。Eric同时有React和Vue两派前端框架的使用经验，了解前端响应式布局的思想，能够进行基础的前端开发，使用过AntDesign等组件库，Pinia等状态管理框架，axios等异步通信框架。\nAI\nEric有各种AI框架的使用经验，对yolo等图形识别ai，一些tts ai进行过基本的使用。Eric了解最简单的ReAct等Agent思路，了解基础的ai云服务api调用，尝试过自己做简单的agent软件。\n志趣 # Eric主要对数据库等底层系统非常感兴趣，但受限于市场与现实，暂且将目光移动到AI Infra，ML Sys等底层与AI训练，推理等过程结合的更加具有前途的方向。\n推荐链接 # winter-raymond的个人wiki\nspiderboy的博客\nSonnyCalcr的博客\n","date":"10 January 2026","externalUrl":null,"permalink":"/posts/about/","section":"Posts","summary":"","title":"about","type":"posts"},{"content":"","date":"10 January 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"10 January 2026","externalUrl":null,"permalink":"/categories/%E7%94%9F%E6%B4%BB/","section":"Categories","summary":"","title":"生活","type":"categories"},{"content":"","date":"10 January 2026","externalUrl":null,"permalink":"/tags/%E8%87%AA%E6%88%91%E4%BB%8B%E7%BB%8D/","section":"Tags","summary":"","title":"自我介绍","type":"tags"},{"content":"","date":"8 November 2025","externalUrl":null,"permalink":"/tags/cpp/","section":"Tags","summary":"","title":"Cpp","type":"tags"},{"content":" 什么是reference_wrapper # reference_wrapper就是引用包装器，它是一个类模板，可以从它生成obj的模板类，使得该类产生的对象具有和obj的引用近乎完全一样的行为。\n为什么要有reference_wrapper # 在cpp中，如std::thread,std::bind这样的类只支持接受值的拷贝，而不接受引用。\n为什么std::thread只接受值拷贝？ # 1. 多线程资源生命周期很危险, std::ref可以强迫程序员知道自己在做什么 # 如果默认使用引用传递(左值引用\u0026amp;或万能引用\u0026amp;\u0026amp;)，则很容易出现生命周期问题 你在某个大括号上下文中传入std::thread一个局部变量的引用，当离开此上下文后，线程完全有可能仍在执行， 而该变量已经被释放 下面代码假设std::thread默认传引用\nvoid do_something(int\u0026amp; a){ std::this_thread::sleep_for(std::chrono::seconds(10));//模拟线程一开始做了一些工作，耗时10s std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; std::endl; } void do_and_start_a_thread(){ int x = 1; std::thread t(do_something, x); t.detach(); std::this_thread::sleep_for(std::chrono::seconds(1));//模拟这里做了一些工作，只用了1s } int main(){ do_and_start_a_thread(); } 从上面的代码看，do_and_start_a_thread()函数启动了线程t并传入局部变量x，我们假设std::thread以引用的格式接受了x， 并且将x传递给了t，此时线程t上的do_something函数和do_and_start_a_thread函数并行，且显然1s后do_and_start_a_thread函数先结束 此时x被释放掉了，而又过了9s，当do_something()运行到访问a的段，a所引用的x却已经被释放掉了吗，错误就会发生。\n上面反映了cpp多线程资源生命周期管理的危险性，所以程序员使用std::ref时，就会意识到自己正在进行多线程中的引用，减小多线程问题发生的概率。\n2. std::thread需要先构造新线程，然后在新线程的上下文中使用参数，这意味着必须把参数先存起来，然而引用是不能存储的。 # 无需多言\n3. 一致性原则 # 这里也没什么好说的，lambda等默认也是值传递，cpp应保持各个方面的一致性。\n总结 # 所以必须要有reference_wrapper这样的类，既可以被作为对象拷贝，又可以作为obj的引用发挥相应的作用。\n实现reference_wrapper # reference_wrapper应具备的功能 # 我们先前说reference_wrapper必须可以作为对象拷贝，同时又可以作为引用发挥作用。\n前者没什么好说，默认拷贝构造函数就可以了。而后者，我们要实现引用的指向原对象的作用，最自然的，当然是想到用指针指向原对象，而如何作为引用使用，只要实现类型转换到T\u0026amp;的重载即可。\n初版代码 # #include \u0026lt;functional\u0026gt; #include \u0026lt;memory\u0026gt; #include \u0026lt;type_traits\u0026gt; #include \u0026lt;utility\u0026gt; template \u0026lt;typename T\u0026gt; class reference_wrapper { private: // 存储引用的指针 T *_ptr; static T *_s_fun(T \u0026amp;ref) noexcept { return \u0026amp;ref; } public: template \u0026lt;typename U\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) : _ptr(_s_fun(ref)) {} reference_wrapper(const reference_wrapper \u0026amp;other) = default; reference_wrapper \u0026amp;operator=(const reference_wrapper \u0026amp;other) = default; operator T \u0026amp;() const noexcept { return this-\u0026gt;get(); } T \u0026amp;get() const noexcept { return *_ptr; } }; 这是一个非常简单，也漏洞百出的模板，在上述代码中，我们实现了构造函数获取指针，存储指针，当需要作为T\u0026amp;使用时自动转换给出原对象的类引用功能，并给出了默认的拷贝构造函数。\n此时，它已经可以使用了。\n#include \u0026#34;eee.h\u0026#34; #include \u0026lt;assert.h\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;vector\u0026gt; void func(int \u0026amp;x) { x = 200; } int main() { int a = 0; reference_wrapper\u0026lt;int\u0026gt; a_r(a); func(a_r); assert(a == 200); } 通过编译\n但是它有着很多问题\n特殊情况下，拷贝构造函数被模板构造函数劫持 # template \u0026lt;typename U\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) : _ptr(_s_fun(ref)) {} reference_wrapper(const reference_wrapper \u0026amp;other) = default; 这段代码中模板构造函数和拷贝构造函数同时存在\n考虑下面的情况\nint s = 0; reference_wrapper\u0026lt;int\u0026gt; a = s; reference_wrapper\u0026lt;int\u0026gt; b(a); 思考b能否被正确构造出来，即reference_wrapper(a)是reference\u0026lt;int\u0026gt;还是reference\u0026lt;reference\u0026lt;int\u0026gt;\u0026gt;。 一般来说是reference\u0026lt;int\u0026gt;的，因为cpp的设计中non-template函数比template的函数优先级高，防止了简单情况下问题的暴露。 然而在复杂情况下仍有可能失败，为了彻底杜绝错误，应当添加匹配，当U是reference_wrapper\u0026lt;T\u0026gt;类型时，触发SFINAE，保证被执行的是拷贝构造函数。\n在类内添加几个模板\ntemplate \u0026lt;typename Tp\u0026gt; using raw_t = typename std::remove_cv_t\u0026lt;std::remove_reference_t\u0026lt;Tp\u0026gt;\u0026gt;; //去除cvr template \u0026lt;typename _T1, typename _T2 = raw_t\u0026lt;_T1\u0026gt;\u0026gt; using not_same = typename std::enable_if\u0026lt;!std::is_same_v\u0026lt;reference_wrapper, _T2\u0026gt;\u0026gt;; template \u0026lt;typename _T1\u0026gt; using not_same_v = not_same\u0026lt;_T1\u0026gt;::type; 然后修改构造函数前的模板判断\ntemplate \u0026lt;typename U, typename = not_same_v\u0026lt;U\u0026gt;\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) : _ptr(_s_fun(ref) {} 上面代码中，raw_t\u0026lt;T\u0026gt;是T去除cvr之后的类型，not_same_v\u0026lt;T\u0026gt;则是要求T去除cvr之后不能等于reference_wrapper\u0026lt;T\u0026gt;(上面代码中只写了reference_wrapper，省略了\u0026lt;T\u0026gt;，这是由于cpp的类型注入)，如果相等，就会触发SFINAE，则模板构造函数失效，编译器会寻找拷贝构造函数等其他合适的函数来执行。 这样的模板保证了模板构造函数无论如何不可能劫持拷贝构造函数\n无法优雅的去除不能转换成T *的类型 # 必须注意到类的模板签名中的类型T和构造函数模板签名中的类型U不是同一个类型\ntemplate \u0026lt;typename T\u0026gt; class reference_wrapper { ...... public: template \u0026lt;typename U, typename = not_same_v\u0026lt;U\u0026gt;, typename = decltype(_s_fun(std::declval\u0026lt;U\u0026gt;()))\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) noexcept(noexcept(_s_fun(std::declval\u0026lt;U\u0026gt;()))) : _ptr(_s_fun( std::forward\u0026lt;U\u0026gt;(ref)) /*完美转发防止使得左右值对号入座进入不同_s_fun函数，这是右值被拒绝的关键条件*/) {} ...... }; 当传入的类型错误时，比如说\ndouble s = 0; reference\u0026lt;char\u0026gt; r(s); 编译器会报错，但这种错误是类型U参与了重载决议，然后编译器发现无法编译，报出硬错误\n这是不符合设计哲学的，更好的办法应该是通过SFINAE，在一开始就告诉编译器，这个U不能参与重载，它不符合要求，所以你去找别的函数吧。最后编译器由于找不到别的函数报出软错误，这就要更好一些。\n于是做出如下改进\ntemplate \u0026lt;typename U, typename = not_same_v\u0026lt;U\u0026gt;, typename = decltype(_s_fun(std::declval\u0026lt;U\u0026gt;()))\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref): _ptr(_s_fun(ref)) { std::declval代表U类型的一个值，然后_s_fun()象征接受这个值，测试是否能成功通过该函数，如果可以才允许U参与重载，否则SFINAE\nno_except的传递 # 我们默认构造函数都是有exception的，然而对于一些类型，构造函数是不存在exception的，这种情况下应将构造函数设为no_except，帮助编译器优化\ntemplate \u0026lt;typename U, typename = not_same_v\u0026lt;U\u0026gt;, typename = decltype(_s_fun(std::declval\u0026lt;U\u0026gt;()))\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) noexcept(noexcept(_s_fun(std::declval\u0026lt;U\u0026gt;()))) : _ptr(_s_fun(ref)) {} 内部的no_except测试_s_fun()函数对相应U的值是否no_except，如果是则外部的no_except被设为true。\n防止传入右值 # 保存右值的引用是危险的，所以我们要防止保存右值的引用\nstatic T *_s_fun(T \u0026amp;\u0026amp;) = delete; template \u0026lt;typename U, typename = not_same_v\u0026lt;U\u0026gt;, typename = decltype(_s_fun(std::declval\u0026lt;U\u0026gt;()))\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) noexcept(noexcept(_s_fun(std::declval\u0026lt;U\u0026gt;()))) : _ptr(_s_fun( std::forward\u0026lt;U\u0026gt;(ref)) /*完美转发防止使得左右值对号入座进入不同_s_fun函数，这是右值被拒绝的关键条件*/) {} 我们做了两件事，一是删除_s_fun的右值引用版本，二是将构造函数中给_ptr赋值处将传给_s_fun()的ref进行完美转发。 完美转发使得传给_s_fun的右值仍是右值，保证右值会被拒绝，触发SFINAE\n使用安全的取址，防止重载\u0026amp;导致的错误 # 考虑以下代码，由于\u0026amp;运算符被重载，我们本来的取地址操作恒定被给出0作为结果。\nstruct Bad{ Bad * operator \u0026amp;(){ return 0; } int a = 10; } int main(){ Bad bad; reference_wrapper\u0026lt;Bad\u0026gt; bad_r(bad); std::cout \u0026lt;\u0026lt; r-\u0026gt;get().a; } 这显然是不对的\n修改原代码为\nstatic T *_s_fun(T \u0026amp;ref) noexcept { return std::addressof(ref); // 使用std::addressof防止重载operator\u0026amp;导致的问题 } // 删除右值引用版本，防止传入临时对象 static T *_s_fun(T \u0026amp;\u0026amp;) = delete; 针对可调用对象的调用处理 # 注意到可调用对象也可以被取引用，并且他们的引用也可以被调用，因此reference_wrapper需要重载()运算符，以支持调用功能\ntemplate \u0026lt;typename... Args\u0026gt; typename std::__invoke_result\u0026lt;T \u0026amp;, Args...\u0026gt;::type operator()(Args \u0026amp;\u0026amp;...args) const noexcept(std::__is_nothrow_invocable\u0026lt;T \u0026amp;, Args...\u0026gt;::value) { return std::__invoke(get(), std::forward(args)...); } 最后给出方便的工厂 # template \u0026lt;typename T\u0026gt; reference_wrapper(T \u0026amp;) -\u0026gt; reference_wrapper\u0026lt;T\u0026gt;; template \u0026lt;typename T\u0026gt; using raw_T = typename std::remove_cv_t\u0026lt;std::remove_reference_t\u0026lt;T\u0026gt;\u0026gt;; template \u0026lt;typename T\u0026gt; inline reference_wrapper\u0026lt;T\u0026gt; my_ref(T \u0026amp;i) noexcept { return reference_wrapper\u0026lt;raw_T\u0026lt;T\u0026gt;\u0026gt;(i); } template \u0026lt;typename T\u0026gt; inline reference_wrapper\u0026lt;T\u0026gt; my_cref(const T \u0026amp;i) noexcept { return reference_wrapper\u0026lt;const raw_T\u0026lt;T\u0026gt;\u0026gt;(i); } template \u0026lt;typename T\u0026gt; void my_ref(const T \u0026amp;\u0026amp;) = delete; template \u0026lt;typename T\u0026gt; void my_cref(const T \u0026amp;\u0026amp;) = delete; template \u0026lt;typename T\u0026gt; inline reference_wrapper\u0026lt;T\u0026gt; my_ref(reference_wrapper\u0026lt;T\u0026gt; _t) noexcept { return _t; } template \u0026lt;typename T\u0026gt; inline const reference_wrapper\u0026lt;T\u0026gt; my_cref(reference_wrapper\u0026lt;T\u0026gt; _t) noexcept { return {_t.get()}; } 最后两个工厂重载是为了防止wrapper包裹wrapper\n完成体 # #include \u0026lt;functional\u0026gt; #include \u0026lt;memory\u0026gt; #include \u0026lt;type_traits\u0026gt; #include \u0026lt;utility\u0026gt; template \u0026lt;typename T\u0026gt; class reference_wrapper { // 去除引用和const volatile修饰 template \u0026lt;typename Tp\u0026gt; using raw_t = typename std::remove_cv_t\u0026lt;std::remove_reference_t\u0026lt;Tp\u0026gt;\u0026gt;; // SFINAE判断是否传入的T与reference_wrapper相同类型 template \u0026lt;typename _T1, typename _T2 = raw_t\u0026lt;_T1\u0026gt;\u0026gt; using not_same = typename std::enable_if\u0026lt;!std::is_same_v\u0026lt;reference_wrapper, _T2\u0026gt;\u0026gt;; template \u0026lt;typename _T1\u0026gt; using not_same_v = not_same\u0026lt;_T1\u0026gt;::type; private: // 存储引用的指针 T *_ptr; static T *_s_fun(T \u0026amp;ref) noexcept { return std::addressof(ref); // 使用std::addressof防止重载operator\u0026amp;导致的问题 } // 删除右值引用版本，防止传入临时对象 static T *_s_fun(T \u0026amp;\u0026amp;) = delete; public: // not_same_v用于当传入的U类型与reference_wrapper相同时，触发SFINAE，防止普通构造函数被触发 // 使得可以正常调用拷贝构造函数 // decltype用于检测_s_fun能否被调用(是否允许U取地址被转化为T*)，若不能则触发SFINAE失败 template \u0026lt;typename U, typename = not_same_v\u0026lt;U\u0026gt;, typename = decltype(_s_fun(std::declval\u0026lt;U\u0026gt;()))\u0026gt; explicit reference_wrapper(U \u0026amp;\u0026amp;ref) noexcept(noexcept(_s_fun(std::declval\u0026lt;U\u0026gt;()))) : _ptr(_s_fun( std::forward\u0026lt;U\u0026gt;(ref)) /*完美转发防止使得左右值对号入座进入不同_s_fun函数，这是右值被拒绝的关键条件*/) {} reference_wrapper(const reference_wrapper \u0026amp;other) = default; reference_wrapper \u0026amp;operator=(const reference_wrapper \u0026amp;other) = default; operator T \u0026amp;() const noexcept { return this-\u0026gt;get(); } T \u0026amp;get() const noexcept { return *_ptr; } // 针对函数调用运算符的重载，使可调用对象的引用包装器仍可以被调用 template \u0026lt;typename... Args\u0026gt; typename std::__invoke_result\u0026lt;T \u0026amp;, Args...\u0026gt;::type operator()(Args \u0026amp;\u0026amp;...args) const noexcept(std::__is_nothrow_invocable\u0026lt;T \u0026amp;, Args...\u0026gt;::value) { return std::__invoke(get(), std::forward(args)...); } }; template \u0026lt;typename T\u0026gt; reference_wrapper(T \u0026amp;) -\u0026gt; reference_wrapper\u0026lt;T\u0026gt;; template \u0026lt;typename T\u0026gt; using raw_T = typename std::remove_cv_t\u0026lt;std::remove_reference_t\u0026lt;T\u0026gt;\u0026gt;; template \u0026lt;typename T\u0026gt; inline reference_wrapper\u0026lt;T\u0026gt; my_ref(T \u0026amp;i) noexcept { return reference_wrapper\u0026lt;raw_T\u0026lt;T\u0026gt;\u0026gt;(i); } template \u0026lt;typename T\u0026gt; inline reference_wrapper\u0026lt;T\u0026gt; my_cref(const T \u0026amp;i) noexcept { return reference_wrapper\u0026lt;const raw_T\u0026lt;T\u0026gt;\u0026gt;(i); } template \u0026lt;typename T\u0026gt; void my_ref(const T \u0026amp;\u0026amp;) = delete; template \u0026lt;typename T\u0026gt; void my_cref(const T \u0026amp;\u0026amp;) = delete; template \u0026lt;typename T\u0026gt; inline reference_wrapper\u0026lt;T\u0026gt; my_ref(reference_wrapper\u0026lt;T\u0026gt; _t) noexcept { return _t; } template \u0026lt;typename T\u0026gt; inline const reference_wrapper\u0026lt;T\u0026gt; my_cref(reference_wrapper\u0026lt;T\u0026gt; _t) noexcept { return {_t.get()}; } ","date":"8 November 2025","externalUrl":null,"permalink":"/posts/1762602009775-post17/","section":"Posts","summary":"","title":"reference_wrapper 的模板实现","type":"posts"},{"content":"你有没有过这样的经历？\n在Unity里，手动用坐标判断两个物体是否碰撞，写了几十行边界检测逻辑，结果朋友一句“你为什么不用Collider？”让你愣在原地； 在Cocos2d-x中，手搓了一套策略模式来实现组件功能切换，后来才发现cocos2d::Component早就内置了完整的组件系统； 在Vue项目里，一层层emit传状态，父子孙曾组件通信像打电报，直到某天听说了Pinia，才意识到“原来状态可以集中管理”； 甚至更经典的：C语言初学者写了100行的printf(\u0026ldquo;1\u0026rdquo;)、printf(\u0026ldquo;2\u0026rdquo;)……却从未想过“循环”这种东西存在。 我们不是能力不足，也不是不愿学习——而是根本不知道“这个问题已经被优雅地解决了”。 这种“我不知道我不知道”的状态，在认知科学中被称为 “未知的未知”（Unknown Unknowns）。它比“已知的未知”更危险，因为你连提问的方向都没有。\n为什么我们总在重复造轮子 # 1. 问题没有被明确定义，就没有办法搜索解决方案 # 明确的定义一个问题并不是非常自然的做法，因为定义问题意味着去抽象和界定，它需要保持很大的清醒，而这种状态往往是很难得的。所以面对问题时，我们大部分时间是没有足够清醒到去明确定义它的。\n就像我们在刚开始写vue的时候，我们在使用emit时心里想的是把子组件的数据传给父组件，乃至后面让曾孙组件把数据给孙组件，再让孙组件把数据给子组件，再父组件\u0026hellip;我们可能会觉得这样写很难受，但是我们的状态没有清醒到能意识到这是一个“全局状态管理问题”，也就没有想法去搜索全局状态管理，从而认识pinia。\n2. 大脑本身具有“舒适区” # 心理学上有个概念叫 “功能固着”：人倾向于用熟悉的方式解决问题，哪怕效率低下。\n你已经学会了if-else，那么你遇到多分支就堆砌逻辑； 你熟悉emit，你需要通信就靠事件冒泡； 你会面向对象，遇到可替换行为就自己手写封装。\n这不是懒惰，而是大脑的节能机制\u0026ndash;调用已有技能比学习新范式更不需要打破舒适区。\n3. 学习路径缺失 # 很多技术都缺少合适的学习资源，所以自己探索时总是会漏过很多内容，产生很多“未知的未知”。\n即使是比较热门的技术，其入门教程往往也只教让非常简易的项目跑起来，而不是工程实践中到底怎么用\n我们只学了最基础的用法，当我们初次去做项目时，也只会用最简单的用法。\n结果就是：我们在小 demo 阶段养成习惯，等项目复杂了才意识到问题，但重构成本已高。\n4. 框架的轮子藏的太深 # 就像cocos2dx的cocos2d::Component组件一样，在cocos官方的教程中一嘴都没提，如果不是查cocos2dx api我根本不知到这个东西的存在。\n如何训练自己看见轮子 # 训练问题敏感度和抽象能力 # 在开发过程中做一个“讲究人”，一旦遇到不舒服的情况立马总结不舒服的原因，并且查问题。\n例子：\n使用传统MVC模式发现不得不写很多胶水代码，核心业务不聚焦，耦合高=\u0026gt;不舒服=\u0026gt;设计模式问题，MVC不舒服的解法=\u0026gt;认识MVVM架构，采用前后端分离MVVM+RESTful API=\u0026gt;解决问题，认知拓宽\n写了很多层emit和props，组件里各种通信变量太乱=\u0026gt;不顺眼=\u0026gt;变量和函数的全局状态管理=\u0026gt;了解pinia，引入pinia，项目大大优化，认识增加\n养成先问再写好习惯 # 在开始处理这个模块之前，先问自己\n这个功能有没有前人提供好的工程实践； 业界通常怎么做； 有没有官方或者社区公认的最佳实践。 有时候五分钟的搜索可以省下五小时的弯路。\n理解框架的设计哲学 # Unity 信奉 “组合优于继承” → 多用 Component，少写继承树 Vue 3 + Pinia 倡导 “状态集中 + 响应式” → 少用深层 emit React 鼓励 “状态提升 + 自定义 Hook” → 避免 prop drilling 理解框架的思想，就知道和这些思想冲突的实践通常都有更好的做法\n多实践，多交流 # 多多实践，多做工程，渐渐地见识就会广大，知识就会丰富，很多一开始“未知的未知”，也就成了常识般的“已知”。\n多交流则是说多和领域错开的，或是同领域做的更深入的人交流，从而从其他人身上吸收最佳实践，形成“问题-方案”映射库。\n结语 # 人要善于调用人类集体的智慧，不只是写代码。“从零开始干”，“从头创造”常常被人赞扬，但是真正的智慧者，应当是在识别问题模式之后，精确地调用已有的高效解决方案。\n很多时候我们缺的不是能力，而是“知道这个问题已经被解决过”的元认知。\n下次当你在工程中准备手写一个通用功能时，不妨先停下来，想一想：\n这个轮子，是不是早有人造好了？\n如果答案是“是”\u0026mdash;恭喜你，又省下了一个周末\n愿我们少造轮子，多造世界。\n以上观点仅针对工程实践，歌颂所有造轮子的贡献者。\n","date":"2 October 2025","externalUrl":null,"permalink":"/posts/1759422197072-post15/","section":"Posts","summary":"","title":"编程中的“未知的未知”：为什么我们总在手搓已有的解决方案？","type":"posts"},{"content":" 博客迁移 # 本来这两天又是写前后端又是学rust的，感觉需要写两篇博客记录一下成长了，结果突然发现老博客拓展性太差了，刚好发现blowfish这个更智能，更强大的基于hugo的博客框架，于是干脆按照blowfish上的教程重新配置了一遍，然后将原来博客的内容全部迁移到新博客了。\nblowfish yyds\n猛猛推荐了\n","date":"30 September 2025","externalUrl":null,"permalink":"/posts/1759232950429-post13/","section":"Posts","summary":"","title":"博客2.0启动","type":"posts"},{"content":"","date":"30 September 2025","externalUrl":null,"permalink":"/tags/%E5%A4%8F%E5%AD%A3%E5%85%AB%E6%AD%A3/","section":"Tags","summary":"","title":"夏季八正","type":"tags"},{"content":"","date":"30 September 2025","externalUrl":null,"permalink":"/categories/%E6%8A%98%E8%85%BE/","section":"Categories","summary":"","title":"折腾","type":"categories"},{"content":" 起因 # 故事的起因是我的另一篇博文，那篇博文只有构思，而没有考虑实际使用Qt/C++ Postgresql来实现这样一个软件难度是非常大的。\n然而刚好最近在看C#，也开始了解了ASP.NET Core Web API，又恰好我学过一点点Postgresql，并且想要了解后端架构，于是我认为我有充分的能力去构建项目的后端。\n至于前端部分，我对h5,js,css的基础内容已经了解，在现代前端中基本上依赖vue,axios,tailwind也就能很简单的实现功能了。而恰好我对vue的基础的数据绑定略有认识，同时axios的基础使用非常简单，想必前端开发也不在话下。\n当然后面的展开证明我不会的部分还是很多的，在实战过程中也接触到好用的tailwind组件库daisyUi，vue开发状态管理库pinia。后端也见到了数据库Migration等有趣的技术，拓宽了视野。\n技术栈总结\n前端：Vue 3 (Composition API) + Pinia + Axios + Tailwind CSS + daisyUI 后端：ASP.NET Core Web API + Entity Framework Core 数据库：PostgreSQL 工具链：VS Code / swagger, Rider, Git 前端开发过程中的收获 # vue # 对vue组件管理有了更深刻的认识。Vue 是一个渐进式 JavaScript 框架，核心优势在于组件化开发与响应式数据绑定，能够高效地构建用户界面。有父子组件的嵌套，就必然遇到子组件父组件通信的问题。vue3中通常父组件通过props或者slots向子组件传递数据，而子组件主要通过emit向父组件传递数据。\n在具体的使用过程中遇到比较多的问题是常常会忘记ref对象必须使用value，而不能用对象本身。因为ref 是响应式引用，必须通过 .value 访问/赋值，否则会破坏响应式链。在模板中使用 ref 时，Vue 会自动解包（无需 .value），但在 script setup标签中必须显式使用。\nconst obj = ref(null) obj.value = \u0026#34;foo\u0026#34; // 正确，修改响应式数据 obj = \u0026#34;foo\u0026#34; // 错误，丢失响应式能力 虽然这可以说是一个非常没有技术含量的问题，但是作为新手的确会经常地写错。\n在使用vue时还遇到一个非常棘手的问题，就是全局状态管理问题。\n如上文所说，本身的vue只能通过父组件向子组件传props/slots和子组件向父组件传emit来进行数据的上下流动，然而当你遇到多个组件嵌套\n\u0026lt;!-- App.vue --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;Compo1 /\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;!-- Compo1.vue --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;Compo2 /\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;!-- Compo2.vue --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;!-- ...... --\u0026gt; \u0026lt;template\u0026gt; 我们且不考虑更复杂的情况，单单是跨了一层的嵌套，如果你现在想要Compo2.vue获得并执行App.vue的一个函数(比如说axios向服务器发起通信请求，如果将 API 调用逻辑集中在根组件，会导致深层组件难以复用)，你就不得不通过props将函数一层一层传下去\n\u0026lt;!-- App.vue --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;Compo1 :getTasks=\u0026#34;getTasks\u0026#34;/\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;!-- Compo1.vue --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;Compo2 :getTasks=\u0026#34;getTasks/\u0026gt; \u0026lt;/template\u0026gt; \u0026lt;!-- Compo2.vue --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;button @click=\u0026#34;handleTasks\u0026#34;\u0026gt;\u0026lt;/button\u0026gt; \u0026lt;!-- ...... --\u0026gt; \u0026lt;template\u0026gt; \u0026lt;script setup\u0026gt; defineProps([\u0026#39;getTasks\u0026#39;]) const handleTasks = async ()=\u0026gt;{ // 执行传进来的getTasks函数 } \u0026lt;/script\u0026gt; 这仅仅是两层嵌套的情况，实际开发中可能有更多层嵌套，各种嵌套间要传递各种函数，这种情况一定是糟糕的，我需要更好的办法解决这种问题，在问了ai之后也的确得到了满意的回答\npinia # Pinia 是 Vue 官方推荐的状态管理库，替代了 Vuex，利用pinia的defineStore函数就可以定义全局状态，在任何组件中都可以直接通过函数获得相应store而使用全局函数和读写全局状态。在一开始我还迟疑是否要向项目引入pinia，因为担心项目变得更复杂，但是在看过pinia官网后，我的疑虑打消了，pinia的使用非常简单，基本使用看了两页就可以直接上手了，因此我毫不犹豫地引入了pinia。\njs的槽点 # js的一大槽点是作为一个前端语言，它很不严谨，可能触发各种各样的问题，让你的页面表现不正常。如果有机会的话以后一定要学学ts\naxios # axios确实是一个非常好用非常简单的框架，\n// 定义全局状态 export const useTaskStore = defineStore(\u0026#39;tasks\u0026#39;, { state: () =\u0026gt; ({ tasks: [], loading: false, error: null, selectedTask: null, }),//全局变量 actions: { async fetchTasks() { this.loading = true this.error = null try { const response = await axios.get(\u0026#39;http://localhost:5005/taskitems\u0026#39;) this.tasks = response.data; } catch { this.error = \u0026#34;get failed\u0026#34; console.error(this.error) } finally { this.loading = false } }, async postTask(task){ this.loading = true this.error = null try{ const response = await axios.post(\u0026#39;http://localhost:5005/taskitems\u0026#39;,task) this.tasks.unshift(response.data) }catch{ this.error = \u0026#34;post failed\u0026#34; console.error(this.error) }finally{ this.loading = false } }, async updateTask(task){ this.loading = true this.error = null try{ const response = await axios.put(\u0026#39;http://localhost:5005/taskitems/\u0026#39;+task.id,task) const index = this.tasks.findIndex(t=\u0026gt;t.id===task.id) if(index != -1){ this.tasks[index] = response.data this.selectedTask = response.data } } catch{ this.error = \u0026#34;put failed\u0026#34; console.error(this.error) } finally{ this.loading = false; } } , async deleteTask(id){ this.loading = true this.error = null try{ await axios.delete(\u0026#39;http://localhost:5005/taskitems/\u0026#39;+id) const index = this.tasks.findIndex(t =\u0026gt; t.id === id); if (index !== -1) { this.tasks.splice(index, 1) } } catch{ this.error = \u0026#34;put failed\u0026#34; console.error(this.error) } finally{ this.loading = false; } } }// 全局函数 }) Note: 这里为了短期方便直接使用了应编码，实际开发更推荐axios.create({ baseURL: \u0026hellip; })，然后单独全局配置baseURL，方便统一管理API地址\n可见只要非常简单的使用axios的函数就可以实现发送请求的基本功能。\ntailwindcss和daisyUI # 虽然我在做很多小组任务时对页面外观都几乎没有要求，只要实现功能就行。但是自己做的东西，还是希望做的让自己满意，所以在询问ai之后，ai推荐使用tailwindcss和daisyUI进行外观配置\n首先tailwindcss是一个主打utility式的css的工具，它主要的优越之处在于抛弃了原本的先写各种class使用的思路，而是使用写好的utility，独立而分散的配置每一个标签的外观。而daisyUI是一个基于tailwindcss的组件库，它预先定义了很多组件的外观和配置，如卡片，列表等可以直接使用，并且daisyUI 提供了主题切换能力，适合快速构建美观界面。总的来说使用tailwind的外观设计还是很顺利的。\n后端部分的收获 # 这次后端开发是我首次使用 ASP.NET Core 构建 RESTful API。虽然此前接触过 Java Spring，但 C# 的生态和工具链仍带来不少新鲜感。通过实践，我接触到了 Minimal API、依赖注入、数据库迁移、跨域配置以及 API 文档生成等技术部分\nMinimal API # ASP.NET Core 的 Minimal APIs 是一种轻量级 API 开发模式，无需创建控制器类，直接在 Program.cs 中通过方法链定义路由和处理逻辑，极大简化了小型项目的搭建流程\n构建RESTfulAPI的过程就是向app的map函数传回调函数\napp.MapGet(\u0026#34;foo\u0026#34;,async(TaskDb db)=\u0026gt;{ //...处理请求 }) 在这样的基础下，开发过程是十分简单的\n配置数据库 # 配置数据库的过程还是相当简单的，首先我们要配置数据对象\n// TaskItem.cs public class TaskItem { public int Id { get; set; } public required string Title { get; set; } public string? Content { get; set; } public string? Status { get; set; } public string? Owner { get; set; } public DateTime CreatedAt { get; set; } } 再去配置我们的针对这种对象的数据库上下文（什么是数据库上下文，见下一节DI容器）\nusing Microsoft.EntityFrameworkCore; // TaskDb继承DbContext class TaskDb : DbContext { public TaskDb(DbContextOptions\u0026lt;TaskDb\u0026gt; options) : base(options) { } public DbSet\u0026lt;TaskItem\u0026gt; Tasks =\u0026gt; Set\u0026lt;TaskItem\u0026gt;(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity\u0026lt;TaskItem\u0026gt;(entity =\u0026gt; { entity.HasKey(e =\u0026gt; e.Id); entity.Property(e =\u0026gt; e.Id).ValueGeneratedOnAdd(); }); } } 在这里我们指定Id是自增的，防止主键相同发生冲突\nDI容器 # 就和JavaSpring有一样，C#也有依赖注入，控制反转容器。并且ASP.NET Core 内置了轻量级的依赖注入容器（Microsoft.Extensions.DependencyInjection），无需额外引入 Spring 等框架，开箱即用。\nusing Microsoft.Extensions.DependencyInjection; 在我看来，IoC容器所做的事就是为各种类自动注入依赖对象并管理对象的生命周期。这样项目的很多上下文依赖都继承特定的上下文部分，然后就可以在容器中注册，由容器来管理依赖和生命周期，简化了管理依赖的困难。\n而ASP.NET WebApplication的构建采用构造器模式，先创建构造器，再在构造器中设置各种app的配置，配置完毕后再生成app对象，而依赖管理等这一步就是在构造器中设置的\nvar builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // 通过AddDbContext\u0026lt;TaskDb\u0026gt;把TaskDb添加到容器中 builder.Services.AddDbContext\u0026lt;TaskDb\u0026gt;(opt =\u0026gt; { opt.UseNpgsql(builder.Configuration.GetConnectionString(\u0026#34;DefaultConnection\u0026#34;)); }); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); 而builder.Services就是一个IServiceCollection，我们通过AddDbContext方法将TaskDb设置为我们要使用的数据库上下文依赖，同时设置数据库连接参数为appsettings.json里面的DefaultConnection这个字符串。\n跨域 # 当后端写好get方法，从前端访问却总是遇到问题，那就是日常CORS问题，即服务器总是禁止跨域请求\n这里我们通过一句代码解决\nbuilder.Services.AddCors(options =\u0026gt; { options.AddPolicy(\u0026#34;AllowVueDev\u0026#34;, policy =\u0026gt; { policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); // 生产环境中应限制具体域名，避免安全风险 }); }); swagger # 我使用 Microsoft.AspNetCore.OpenApi 和 Swashbuckle.AspNetCore 自动生成并可视化 API 文档（Swagger UI），并在开发环境启用交互式 UI（类似 Swagger UI），方便调试接口。\n数据库迁移 # 通过 EF Core 的迁移功能，我可以将 C# 模型变更同步到数据库。例如，添加新字段后，只需运行\ndotnet ef migrations add AddTaskStatus dotnet ef database update 就可以同步C#模型到数据库，而不用手写sql。\n迁移文件是代码化的数据库变更记录，可纳入版本控制，实现数据库版本管理，这种像管理Git项目一样管理数据库结构的做法，初见不可谓不惊艳。\n思想架构分析 # 其实架构分析往往不在开发后才分析，而是在开发前就学习，开发后再总结，不过这里省略开发前的学习，直接谈实战接触后的认识了。\n前端架构 # 传统MVC模型 # 先在开始讲现代项目架构之前，先解释一下传统的MVC模型。\nMVC是在前后端尚不分离时就有的经典模型，非常经典的例子就是我当初写过的Java Spring+Hibernate+Struts技术栈。Struts 是 MVC 的 Web 层实现，Spring 管理业务逻辑（Service），Hibernate 管理数据访问（DAO），三者共同构成分层架构。\nMVC中M指模型，V指视图，而C是控制器\nM是后端的具体业务，如数据库的管理等，V是前端显示的视图。MVC的在当初的卓越在于它在前后端不分离(jsp)时代就有了让前端和后端尽可能分开的意识。它要求前端不能直接去访问后端，而是通过控制器Controller去访问后端。前端视图View只能调用Controller，Controller再去调用模型Model中的业务代码处理业务。\nMVC的缺陷 # 随着MVC项目的不断发展，就不得不写出大量Controller胶水代码来黏合M和V，同时使得Controller的这对职责过多，容易成为上帝类，对项目管理产生很大的挑战，也降低了开发效率。\n前端MVVM模型 # 现代前端如vue采用的架构叫MVVM架构，它由三个部分M,V,VM构成。 它其实很好理解，但首先让我们抛掉MVC的包袱。\n其中M为模型，V为视图，而VM则叫模型视图。\n我们先给出综述：\n首先MVVM模式强依赖于数据绑定，它的核心思路是M中存着前端业务代码（如各种向后端发送请求的逻辑，前端存有的核心数据等），而VM持有希望暴露给视图的代码，V则是视图呈现。\n这里的M，V延续了MVC中的词汇，然而和传统MVC中的MV并不相同。最大的区别就是应用场景从前后端变成了仅前端。这里的M是前端的M，不是后端的M。这句话的意思是，在传统MVC模型中M是后端的核心逻辑(如通过ORM向数据库中添加一名学生)，V是大前端，而前端MVC是单针对于前端说的，这里的 M 是前端的数据模型（如通过 axios 向 /student 发送 POST 请求），而具体的后端持久化逻辑（如 ORM 操作）对前端是透明的。而V仅仅指显示出去的视图。\n那么什么是VM呢，View-Model视图模型，其实名字上已经能看出，它就是指视图的模型。我们不直接把模型中的数据给到视图来显示，而是通过把数据给到VM，VM再把数据给到V来显示。\n前面说过，MVVM强依赖数据绑定，这正是在VM中体现的，VM会监听 Model 的变化并自动更新 View，同时也处理用户的交互（如点击、输入）并更新 Model。\nMVVM模型的好处 # 由于VM通过响应式数据自动更新视图，用户交互通过事件更新VM状态，自动同步M和V的状态，减少了手动操作DOM的样板代码，使代码整洁，降低出错率。\n更加模块化，职责更清晰。VM专注业务状态和逻辑，不需直接操作DOM;V只负责展示，通过声明式语法和VM通信；M只包含核心数据和逻辑。\n易测试。VM是纯js对象，不依赖DOM，可以方便地做单元测试。\n响应式数据天然更适合响应式编程。\n后端架构 # 该项目目前后端过于简单，只是参考了RESTful API和Minimal API，实际上还没有采用Clean Architecture架构。还没有划分DTO，所以该博客省略该部分。\n未来可引入 DTO（Data Transfer Object） 隔离数据库实体与 API 模型，提升安全性与灵活性。\n","date":"30 September 2025","externalUrl":null,"permalink":"/posts/1759239394060-post14/","section":"Posts","summary":"","title":"从零构建任务管理应用：我的前后端分离初体验","type":"posts"},{"content":"","date":"2 September 2025","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":" trs # 今天花了几个小时从零折腾一些库，开发了一个python翻译工具脚本，当读入单词时请求bing网页再用bs处理得到词的解释，当读入句子时请求大模型得到智能翻译，并使用rich库美化了输出。\n仓库地址\n并且从中学习到了python脚本如何转换为系统自动调用python运行的脚本\n即在python脚本头添加shebang头，然后将python脚本的名称中的.py删去即可\n二次开发 # 2025/9/3 11:43\n在群友的建议下，晚上又花了一些时间在不影响原本功能的情况下添加了复制和保存功能\n","date":"2 September 2025","externalUrl":null,"permalink":"/posts/1759232939643-post11/","section":"Posts","summary":"","title":"trs","type":"posts"},{"content":"","date":"2 September 2025","externalUrl":null,"permalink":"/tags/%E5%B0%8F%E4%B8%9C%E8%A5%BF/","section":"Tags","summary":"","title":"小东西","type":"tags"},{"content":"","date":"2 September 2025","externalUrl":null,"permalink":"/categories/%E5%BC%80%E5%8F%91/","section":"Categories","summary":"","title":"开发","type":"categories"},{"content":"","date":"31 August 2025","externalUrl":null,"permalink":"/tags/c%23/","section":"Tags","summary":"","title":"C#","type":"tags"},{"content":"","date":"31 August 2025","externalUrl":null,"permalink":"/tags/oodp/","section":"Tags","summary":"","title":"OODP","type":"tags"},{"content":" 提供一个简明的 API 来逐步构建一个复杂对象，将其构建过程与它的表示分离开。 # 引入 # 先来看一段C#自带的Builder模式的应用\nStringBuilder\n假设我们想要构建Html元素\nstatic void Main(string[] args) { var hello = \u0026#34;hello\u0026#34;; var sb = new StringBuilder(); sb.Append(\u0026#34;\u0026lt;p\u0026gt;\u0026#34;); sb.Append(hello); sb.Append(\u0026#34;\u0026lt;/p\u0026gt;\u0026#34;); Console.WriteLine(sb.ToString()); sb.Clear(); var words = new[] { \u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34; }; sb.Append(\u0026#34;\u0026lt;ul\u0026gt;\u0026#34;); foreach (var word in words) { sb.AppendFormat(\u0026#34;\u0026lt;li\u0026gt;{0}\u0026lt;li\u0026gt;\u0026#34;, word); } sb.Append(\u0026#34;\u0026lt;/ul\u0026gt;\u0026#34;); Console.WriteLine(sb.ToString()); } 我们现在想要写一个HtmlElement类将Html元素封装起来，它需要能够包含标签名，文本和子元素。\npublic class HtmlElement { public string Name, Text; public List\u0026lt;HtmlElement\u0026gt; Elements = []; public HtmlElement() { } public HtmlElement(string name, string text) { Name = name ?? throw new ArgumentNullException(nameof(name)); Text = text ?? throw new ArgumentNullException(nameof(text)); } } 然后我们希望HtmlElement可以优雅的转换为字符串（应包含缩进）\n// 在 HtmlElement 类中 private const int IndentSize = 2; // 定义缩进为两个空格 private string ToStringImpl(int indent) { var sb = new StringBuilder(); var i = new string(\u0026#39; \u0026#39;, indent * IndentSize); sb.AppendLine($\u0026#34;{i}\u0026lt;{Name}\u0026gt;\u0026#34;); if (!string.IsNullOrEmpty(Text)) { sb.Append(new string(\u0026#39; \u0026#39;, (indent+1) * IndentSize)); sb.AppendLine(Text); } foreach (var e in Elements) { string s = e.ToStringImpl(indent + 1); sb.Append(s); } sb.AppendLine($\u0026#34;{i}\u0026lt;/{Name}\u0026gt;\u0026#34;); return sb.ToString(); } //使用递归DFS实现了元素的字符串输出 public override string ToString() { return ToStringImpl(0); } 我们仍然不满意，还想要一个更简单的构建方式，于是我们自己写一个HtmlBuilder类\nHtmlBuilder # 考虑HtmlBuilder应该以一个根元素为基础，可以添加子元素，可以清空子元素\npublic class HtmlBuilder { private readonly string rootName; private HtmlElement _root = new HtmlElement(); public HtmlBuilder(string rootName) { this.rootName = rootName ?? throw new ArgumentNullException(nameof(rootName)); _root.Name = rootName ?? throw new ArgumentNullException(nameof(rootName)); } public void AddChild(string childName ,string childText) { var e = new HtmlElement(childName, childText); _root.Elements.Add(e); } public override string ToString() { return _root.ToString(); } public void Clear() { _root = new HtmlElement{Name = rootName}; } } 上述代码中专门保留rootName是因为我们希望Clear()后仍然保持其根元素\n调用\npublic class Demo { static void Main(string[] args) { var builder = new HtmlBuilder(\u0026#34;ul\u0026#34;); builder.AddChild(\u0026#34;li\u0026#34;,\u0026#34;hello\u0026#34;); builder.AddChild(\u0026#34;li\u0026#34;,\u0026#34;world\u0026#34;); Console.WriteLine(builder.ToString()); } } 我们就得到了较为美观的输出\n\u0026lt;ul\u0026gt; \u0026lt;li\u0026gt; hello \u0026lt;/li\u0026gt; \u0026lt;li\u0026gt; world \u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; 流构建器 # 注意到StringBuilder可以使用一种Fluent Interface Pattern的流调用方法 如\nsb.Append(\u0026#34;a\u0026#34;).Append(\u0026#34;b\u0026#34;).Append(\u0026#34;c\u0026#34;); 我们希望我们的HtmlBuilder也具有这样的功能，只需要\npublic HtmlBuilder AddChild(string childName ,string childText) { var e = new HtmlElement(childName, childText); _root.Elements.Add(e); return this; }//将原来的void返回值改为返回对自身的引用即可 于是我们可以这样调用\nbuilder.AddChild(\u0026#34;li\u0026#34;,\u0026#34;hello\u0026#34;).AddChild(\u0026#34;li\u0026#34;,\u0026#34;world\u0026#34;); 流式构建器的继承 # 问题 # 对普通构建者的继承不会引起什么问题，而当使用流式构建者时，问题则棘手起来。\n先来看一个简单的例子，我们有Person类，它具有名字和职位两个属性。同时有一个流构建者只处理名字。\npublic class Person { public string Name; public string Position; public override string ToString() { return $\u0026#34;{nameof(Name)}: {Name}, {nameof(Position)}: {Position}\u0026#34;; } } public class PersonInfoBuilder { protected Person person = new Person(); //这里是protected因为我们一会将处理继承关系 public PersonInfoBuilder Called(string name) { person.Name = name; return this; } } 现在我们有新的业务需求，即现在需要Builder能同时处理职位的构建，我们遵循开闭原则使用新类继承PersonInfoBuilder\npublic class PersonJobBuilder: PersonInfoBuilder { public PersonJobBuilder WorksAsA(string position) { person.Position = position; return this; } } 现在我们尝试调用PersonJobBuilder\n// bad code PersonJobBuilder jb = new PersonJobBuilder(); jb.Called(\u0026#34;Li Bad\u0026#34;).WorksAsA(\u0026#34;Manager\u0026#34;); 注意到这段代码是报错的，原因是Called方法返回的是一个PersonInfoBuilder，而PersonInfoBuilder不能处理职位相关事项，不具备WorksAsA方法\n问题本质： 当 Fluent 方法需要返回 this 时，在继承链中，父类的方法如果返回的是父类类型 (PersonInfoBuilder)，那么子类的方法链就会在调用父类方法后中断，无法继续调用子类的方法。\n解决方案 # 使用递归泛型\n首先我们来创建一个PersonBuilder的抽象类，将Person的存储和Build方法提取到抽象类里。\npublic abstract class PersonBuilder { protected Person person = new Person(); public Person Build() { return person; } } 然后用递归泛型改写PersonInfoBuilder\npublic class PersonInfoBuilder\u0026lt;TSelf\u0026gt; : PersonBuilder where TSelf : PersonInfoBuilder\u0026lt;TSelf\u0026gt; { public TSelf Called(string name) { person.Name = name; return (TSelf)this; } } 理解： 给PersonInfoBuilder添加了一个泛型TSelf，TSelf用来存放子类的类型，于是Called方法就可以通过类型转换return子类的类型，为了保证TSelf一定是子类的类型，我们用where语句限制TSelf继承于该类。\n我们可能下意识这样使用\n// bad code public class PersonJobBuilder: PersonInfoBuilder\u0026lt;PersonJobBuilder\u0026gt; { public PersonJobBuilder WorksAsA(string position) { person.Position = position; return this; } } 但是假想如果有类又继承PersonJobBuilder的话，由于PersonJobBuilder被固定，继承PersonJobBuilder的类又将不能正确工作。因此这决不是一个好注意。\n正确的写法应该是在PersonJobBuilder上继续泛型\npublic class PersonJobBuilder\u0026lt;TSelf\u0026gt; : PersonInfoBuilder\u0026lt;PersonJobBuilder\u0026lt;TSelf\u0026gt;\u0026gt; where TSelf : PersonJobBuilder\u0026lt;TSelf\u0026gt; { public TSelf WorksAsA(string position) { person.Position = position; return (TSelf)this; } } 理解： PersonJobBuilder也具有类型TSelf的泛型，也用where限制TSelf继承于该类，同时PersonJobBuilder中的泛型从PersonJobBuilder改为PersonJobBuilder，仅此而已\n当我们高兴的尝试使用PersonJobBuilder时，我们发现PersonJobBuilder并不能直接构建。这样的泛型类都不能直接使用，必须用一个类继承泛型类，才能使用。\n所以我们在Person中写一个Builder类，它继承于PersonJobBuilder\u0026lt;Person.Builder\u0026gt;，同时给Person类添加相应的构建方法\n// 在Person类内 public class Builder:PersonJobBuilder\u0026lt;Builder\u0026gt; { } public static Builder New =\u0026gt; new Builder(); 于是我们现在可以调用\nvar person = Person.New. Called(\u0026#34;Wang\u0026#34;). WorksAsA(\u0026#34;LaoBan\u0026#34;). Build(); Console.WriteLine(person); 得到输出\nName: Wang, Position: LaoBan 分步构建器 # 设想这样一个场景，我们现在要做车的创建者，车子具有类型，小轿车(Sedan)或跨界车(CrossOver)，小轿车的尺寸必须在15到17之间，而跨界车必须在17-20之间，如果超出范围就要报错。\n我们注意到该构建过程具有明显的分步性，必须先知道类型，才能判断尺寸，这就引出了分步构建者\n先定义车类\npublic enum CarType { Sedan, Crossover } public class Car { public CarType type; public int WheelSize; } 应用接口隔离原则我们将车的构建过程分成几个独立的接口\npublic interface ISpecifyCarType { ISpecifyWheelSize OfType(CarType type); } public interface ISpecifyWheelSize { IBuildCar WithWheels(int size); } public interface IBuildCar { public Car Build(); } 其中ISpecifyCarType用来指定车的类型，它随后返回一个ISpecifyWheelSize接口进一步指定车的轮子尺寸，再返回IBuildCar进一步构建车对象\n在CarBuilder类中，我们将构建过程的实现封装到Impl里\npublic class CarBuilder { private class Impl:ISpecifyWheelSize,ISpecifyCarType,IBuildCar { private Car _car = new Car(); public ISpecifyWheelSize OfType(CarType type) { _car.type = type; // work as ISpecifyCarType return this; // this is ISpecifyWheelSize } public IBuildCar WithWheels(int size) { switch (_car.type) { case CarType.Crossover when size is \u0026lt; 17 or \u0026gt; 20: case CarType.Sedan when size is \u0026lt; 15 or \u0026gt; 17: throw new ArgumentException(\u0026#34;Car size out of range\u0026#34;); default: break; } _car.WheelSize = size; // work as ISpecifyWheelSize return this; // this is IBuildCar } public Car Build() { return _car; } } public static ISpecifyCarType Create() { return new Impl(); } } 注意到上述Impl类同时实现了ISpecifyWheelSize,ISpecifyCarType,IBuildCar三个接口，所以它只需要不停return this就可以让构建过程不断递进。\n使用如下\nvar car = CarBuilder.Create().OfType(CarType.Sedan).WithWheels(20).Build(); 函数式构建器 # 我们可以使用函数式的方式来构造一个PersonBuilder类，我们让它存储下我们想要对对象进行的所有操作的列表，然后在Build时再对初始对象进行所有这些操作并返回\npublic sealed class PersonBuilder { private List\u0026lt;Func\u0026lt;Person, Person\u0026gt;\u0026gt; actions = new List\u0026lt;Func\u0026lt;Person, Person\u0026gt;\u0026gt;(); private PersonBuilder AddAction(Action\u0026lt;Person\u0026gt; action) { actions.Add(p=\u0026gt; { action(p); return p; }); return this; } public Person Build() =\u0026gt; actions.Aggregate(new Person(), (acc, action) =\u0026gt; action(acc)); public PersonBuilder Called(string name) =\u0026gt; Do(p =\u0026gt; p.Name = name); public PersonBuilder Do(Action\u0026lt;Person\u0026gt; action)=\u0026gt;AddAction(action); } 我们使用sealed关键字密封该类，强调我们不需要继承就能实现想要的功能。\n那么我们如何不通过继承来添加新的功能呢？\n答案是通过C#的静态拓展方法\npublic static class PersonBuilderExtensions { public static PersonBuilder WorksAsA(this PersonBuilder builder, string position) =\u0026gt; builder.Do(p =\u0026gt; p.Position = position); } 此时我们可以直接使用WorksAsA\nvar person = new PersonBuilder().Called(\u0026#34;Wang\u0026#34;).WorksAsA(\u0026#34;LaoBan\u0026#34;).Build(); 注意到，函数式创建者的工作方式其实都是类似的，所以我们直接将函数式构建者写为泛型，然后再特化使用\npublic abstract class FunctionBuilder\u0026lt;TSubject, TSelf\u0026gt; where TSelf : FunctionBuilder\u0026lt;TSubject, TSelf\u0026gt; where TSubject: new() { private List\u0026lt;Func\u0026lt;TSubject, TSubject\u0026gt;\u0026gt; actions = new List\u0026lt;Func\u0026lt;TSubject, TSubject\u0026gt;\u0026gt;(); private TSelf AddAction(Action\u0026lt;TSubject\u0026gt; action) { actions.Add(p=\u0026gt; { action(p); return p; }); return (TSelf)this; } public TSubject Build() =\u0026gt; actions.Aggregate(new TSubject(), (acc, action) =\u0026gt; action(acc)); public TSelf Do(Action\u0026lt;TSubject\u0026gt; action)=\u0026gt;AddAction(action); } public sealed class PersonBuilder:FunctionBuilder\u0026lt;Person,PersonBuilder\u0026gt; { public PersonBuilder Called(string name)=\u0026gt; Do(p =\u0026gt; p.Name = name); } public static class PersonBuilderExtensions { public static PersonBuilder WorksAsA(this PersonBuilder builder, string position) =\u0026gt; builder.Do(p =\u0026gt; p.Position = position); } 功能仍然不变\n分派构建器 # 我们可以将一个对象的属性分拆成不同的方面，交给不同的构建器负责，最后再封装给一个构建器\n例子 # 比如我们有一个Person类，它具有StreetAddress,Postcode,City等居住地相关属性和CompanyName,Position,AnnualIncome等工作相关属性\npublic class Person { public string? StreetAddress,Postcode,City; public string? CompanyName, Position; public int AnnualIncome; public override string ToString() { return $\u0026#34;{nameof(StreetAddress)}: {StreetAddress}, {nameof(Postcode)}: {Postcode}, {nameof(City)}: {City}, {nameof(CompanyName)}: {CompanyName}, {nameof(Position)}: {Position}, {nameof(AnnualIncome)}: {AnnualIncome}\u0026#34;; } } 然后再写一个PersonBuilder类（该类是一个外壳，并不处理具体构建过程）\npublic class PersonBuilder // facade { protected Person Person = new Person(); } 然后我们写PersonAddressBuilder和PersonJobBuilder两个类继承于PersonBuilder，分别处理地址和工作相关的属性的构建\npublic class PersonAddressBuilder : PersonBuilder { public PersonAddressBuilder(Person person) { Person = person; } public PersonAddressBuilder At(string streetAddress) { Person.StreetAddress = streetAddress; return this; } public PersonAddressBuilder WithPostcode(string postcode) { Person.Postcode = postcode; return this; } public PersonAddressBuilder In(string city) { Person.City = city; return this; } } public class PersonJobBuilder:PersonBuilder { public PersonJobBuilder(Person person) { this.Person = person; } public PersonJobBuilder At(string companyName) { Person.CompanyName = companyName; return this; } public PersonJobBuilder AsA(string position) { Person.Position = position; return this; } public PersonJobBuilder Earning(int amount) { Person.AnnualIncome = amount; return this; } } 将PersonJobBuilder和PersonAddressBuilder的对象作为PersonBuilder的公共属性\npublic class PersonBuilder // facade { protected Person Person = new Person(); public PersonJobBuilder Works =\u0026gt; new PersonJobBuilder(Person); public PersonAddressBuilder Lives =\u0026gt; new PersonAddressBuilder(Person); public static implicit operator Person(PersonBuilder b) =\u0026gt; b.Person; } 上面代码中的operator重载了PersonBuilder向Person的隐式转换，使得PersonBuilder构建对象使用时更方便\n于是我们可以这样使用构建器\nvar pb = new PersonBuilder(); Person person = pb.Works.At(\u0026#34;Huawei\u0026#34;).AsA(\u0026#34;Manager\u0026#34;).Earning(1000000) .Lives.At(\u0026#34;Nanluoguxiang\u0026#34;).In(\u0026#34;Beijing\u0026#34;).WithPostcode(\u0026#34;666\u0026#34;); 总结 # 构建器是用来构建对象的独立的组件 你可以通过构造函数生成构建器，也可以通过静态函数返回构建器 如果要让构建器实现流式接口，只需要return this，如果还要实现继承，使用递归泛型 一个对象的不同方面可以由不同的构建器通过一个基类并联起来 ","date":"31 August 2025","externalUrl":null,"permalink":"/posts/1759232934070-post10/","section":"Posts","summary":"","title":"OODP6 构造器模式 (Builder)","type":"posts"},{"content":"","date":"31 August 2025","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF%E7%9B%B8%E5%85%B3/","section":"Categories","summary":"","title":"技术相关","type":"categories"},{"content":" Gamma Categorization # 设计模式通常被分为三类\n这被称为Gamma分类(纪念Erich Gamma)\n创造型模式 Creational Patterns # 处理对象的创建 显示(构造函数)或隐式(依赖注入DI,反射等) 整批的(单语句single statement)或分段的(一步一步地step-by-step) 结构性模式 Structural Patterns # 和结构相关的(如类成员) 许多模式是模仿隐含于它之下的类的接口的包装器 强调良好API设计的重要性 行为型模式 Behavioral Patterns # 没有中心主题 ","date":"31 August 2025","externalUrl":null,"permalink":"/posts/1759232929063-post9/","section":"Posts","summary":"","title":"OODP5 Gamma分类","type":"posts"},{"content":"","date":"31 August 2025","externalUrl":null,"permalink":"/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/","section":"Tags","summary":"","title":"设计模式","type":"tags"},{"content":" 设计代码结构时，高层模块不应该依赖低层模块，二者都应该依赖其抽象。抽象不应该依赖于细节，细节应该依赖于抽象。 # 错误做法 # 我们看一个简单的例子，设计一个关系存储的模块，共有三种关系(Relationship)，父，子，同辈。\n于是我们给出Relationship枚举和Person类\npublic enum Relationship { Parent, Child, Sibling } public class Person { public string Name; } 开始设计低层模块，记录关系的Relationship类，我们随意的使用tuple来记录各种关系，并简单给出添加关系的api，同时将私有的relations成员通过公有的Relations函数暴露出去\npublic class Relationships { private List\u0026lt;(Person, Relationship,Person)\u0026gt; relations = []; public void AddParentAndChild(Person parent, Person child) { relations.Add((parent, Relationship.Parent, child)); relations.Add((child, Relationship.Child, parent)); } public List\u0026lt;(Person, Relationship, Person)\u0026gt; Relations =\u0026gt; relations; } 于是我们在高层模块中使用直接使用底层模块，即在Demo构造函数中直接接受Relationships作为参数，当我们想要查询John的孩子是谁时，我们遍历通过Relations得到的关系元组进行查询输出即可。\npublic class Demo { public Demo(Relationships relationships) { var relations = relationships.Relations; foreach (var r in relations.Where(x =\u0026gt; x.Item1.Name == \u0026#34;John\u0026#34; \u0026amp;\u0026amp; x.Item2 == Relationship.Parent)) { Console.WriteLine($\u0026#34;John has a child {r.Item3.Name}\u0026#34;); } } static void Main(string[] args) { var parent = new Person(){Name = \u0026#34;John\u0026#34;}; var child1 = new Person(){Name = \u0026#34;Jane\u0026#34;}; var child2 = new Person(){Name = \u0026#34;Jonny\u0026#34;}; var relations = new Relationships(); relations.AddParentAndChild(parent, child1); relations.AddParentAndChild(parent, child2); new Demo(relations); } } 反思环节 # 注意到，当我们这样做时，由于高层直接依赖于低层，低层是“不可拆换的”。\n例如，如果我们不想再使用元组列表作为我们的数据结构，而是想要切换成元组数组，或者直接换成树结构。我们的代码就必须大量重写，不仅要完全重写低层记录关系的类，还得将上层Demo类里直接调用低层类的部分全部重写。\n如果这是一个很大的项目架构，这个低层模块可能被很多个高层模块调用，这将带来巨大的维护代价。\n从DIP中总结，即是高层模块直接依赖低层模块(Demo类直接使用Relationships类)，抽象依赖于细节(“从关系中查找特定人的孩子”的抽象依赖于了元组和遍历这样的实现细节)\n正确操作 # 高层模块不应依赖于低层模块，二者都应该依赖于抽象。那么我们需要一个抽象放在Demo和Relationships之间。\n于是我们给出接口IRelationshipBrowser\npublic interface IRelationshipBrowser { IEnumerable\u0026lt;Person\u0026gt; FindAllChildOf(string person); } 这个接口即为抽象的一种形式，它代表着“关系浏览器”的抽象概念，而不涉及任何它的实现，只说明它应该有找到某人所有孩子的功能。\n于是我们现在可以让高层依赖抽象\n这里参数的类型的正是抽象，而不是底层模块本身。与此同时你可以看到API比原来好看的多。\npublic Demo(IRelationshipBrowser browser) { foreach (var r in browser.FindAllChildOf(\u0026#34;John\u0026#34;)) { Console.WriteLine($\u0026#34;John has a child {r.Name}\u0026#34;); } } 低层模块的实现也依赖接口的抽象，即低层模块必须实现该接口的功能。\n/// public class Relationships : IRelationshipBrowser { private List\u0026lt;(Person, Relationship,Person)\u0026gt; relations = [];\npublic void AddParentAndChild(Person parent, Person child) { relations.Add((parent, Relationship.Parent, child)); relations.Add((child, Relationship.Child, parent)); } public IEnumerable\u0026lt;Person\u0026gt; FindAllChildOf(string person) { var relations = Relations; return relations.Where(x =\u0026gt; x.Item1.Name ==person \u0026amp;\u0026amp; x.Item2 == Relationship.Parent).Select(x =\u0026gt; x.Item3); // low level } public List\u0026lt;(Person, Relationship, Person)\u0026gt; Relations =\u0026gt; relations; }\n这里函数的实现使用了LINQ来简化。 注意到此时相比于之前的代码，当我们想要换一种Relationships，如用树來实现同样的功能时，我们不再需要更改上层模块了。此时只需要写一个类，比如说TreeRelationships，让它实现IRelationshipBrowser接口，在所有上层调用中就可以直接使用TreeRelationships类了（因为上层模块接受所有实现了IRelationshipBrowser的类作为参数）。从直观理解上，上层模块只关心下层提供什么样的服务，所以应只依赖于抽象，这样下层即使更改了服务的提供商，只要能提供目标服务，也能正确地运行。 ","date":"30 August 2025","externalUrl":null,"permalink":"/posts/1759232901555-post8/","section":"Posts","summary":"","title":"OODP4 依赖倒置原则(DIP)","type":"posts"},{"content":" 不应强迫客户端依赖它不使用的方法 # 该原则过于简单，索性不放代码\n例子 # 一个现代打印机可以打印(Print),传真(Fax),扫描(Scan)，所以我们写了一个接口IMachine，并让它包括这三个功能的函数\n然而，当我们需要创建一个老式打印机时，该打印机只有打印功能，如果实现IMachine接口，Fax和Scan两个函数要么抛出异常，要么打印未实现，这都是非常不好的情况(即我们强迫老式打印机依赖了它不使用的方法)。\n因此我们要把IMachine接口拆分开，如IPrinter,IFaxer,IScanner，以使得客户端需要使用什么方法就实现什么接口。\n和SRP的区别 # 特性 SRP (单一职责原则) ISP (接口隔离原则) 关注点 类的内部凝聚力 - 一个类应该只做一件事 接口的外部耦合 - 一个接口不应该强迫客户端依赖它不需要的方法 主要对象 类（Class） 接口（Interface） 核心思想 只有一个变化原因 不强迫客户接受不需要的方法 解决的问题 类过于庞大、僵化、难以修改 接口臃肿，导致客户端实现不必要的空方法或收到“接口污染” 关系 相辅相成。通常，遵循SRP拆分出的类，自然会拥有更单一、更聚焦的接口。而为了遵循ISP，你常常需要先运用SRP来厘清职责。 ","date":"29 August 2025","externalUrl":null,"permalink":"/posts/1759232893885-post7/","section":"Posts","summary":"","title":"OODP3 接口隔离原则(ISP)","type":"posts"},{"content":" 父类的对象可以被子类的对象替换，而程序的行为不会发生变化 # 例子 # 我们有Rectangle类\npublic class Rectangle { public int Height{get; set; } public int Width { get; set; } public Rectangle() { } public Rectangle(int width, int height) { Width = width; Height = height; } public override string ToString() { return $\u0026#34;{nameof(Width)}: {Height}, {nameof(Height)}: {Height}\u0026#34;; } } 基于Rectangle类再写Square类\npublic class Square : Rectangle { public new int Width { set { base.Width = base.Height = value; } } public new int Height { set { base.Width = base.Height = value; } } } 注意到我们用new关键字hide了父类的Width和Height\n此时Square和Rectangle都可以正常工作\nRectangle rc = new Rectangle(); rc.Width = 20; rc.Height = 40; Console.WriteLine(\u0026#34;Area: {0}\u0026#34;, Area(rc)); Square sq = new Square(); sq.Width = 4; Console.WriteLine(\u0026#34;Area {0}\u0026#34;, Area(sq)); 可以看到结果正常\n而由于正方形是一种长方形，我们可以使用父类型Rectangle来持有子类型Square的引用。\n那么问题发生了\nRectangle sq = new Square(); sq.Width = 4; Console.WriteLine(\u0026#34;Area {0}\u0026#34;, Area(sq)); 输出: Area 0 这就违反了里氏替换原则，当子类的对象替换父类时发生了错误。\n修改方案 # 将Rectangle中Width和Height的get,set修改为虚，使父类引用执行子类的函数\npublic virtual int Height{get; set; } public virtual int Width { get; set; } 同时将Square中的用new来hide父类型改为用override来重写父类虚函数\npublic override int Width { set { base.Width = base.Height = value; } } public override int Height { set { base.Width = base.Height = value; } } 则现在不会出现错误\n特性 里氏替换原则 (LSP) 虚函数/重写 (virtual/override) 本质 设计原则 (Principle) 语法机制 (Syntax) 目的 指导如何正确地建立继承关系，确保行为兼容性。 实现运行时多态，允许子类定制方法实现。 关系 目标 (What \u0026amp; Why) 主要实现手段 (How) C#, C++和Java的虚函数辨析 # 特性 C# Java 声明可重写方法 使用 virtual 关键字 默认就是“虚”的，无需关键字 重写方法 使用 override 关键字 使用 @Override 注解（最佳实践） 阻止重写 方法不标记为 virtual 使用 final 关键字 隐藏方法（非多态） 使用 new 关键字 无关键字（但不推荐，会产生警告） 特性 C# C++ 默认行为 非虚拟 (方法默认不能被重写) 非虚拟 (方法默认不具有多态性) 实现多态 基类 virtual + 派生类 override 基类 virtual (派生类推荐使用 override) 设计哲学 “默认关闭”：出于安全和性能，需要显式开放重写能力。 “不为未使用的东西付出代价”：需要多态时，显式请求并承担其开销。 你的做法 慎重选择哪些方法设为virtual。只开放需要扩展的点。 必须为所有需要多态的方法加上virtual，否则无法按预期工作。 ","date":"27 August 2025","externalUrl":null,"permalink":"/posts/1759232888597-post6/","section":"Posts","summary":"","title":"OODP2 里氏替换原则(LSP)","type":"posts"},{"content":" 软件实体应对拓展开放，对修改关闭 # 以规格模式为例 # 不好的写法 # public enum Color { Red, Green, Blue, } public enum Size { Small, Medium, Large, Yuge } public class Product { public string Name; public Color Color; public Size Size; public Product(string name, Color color, Size size) { if (name == null) { throw new ArgumentNullException(paramName: nameof(name)); } Name = name; Color = color; Size = size; } } 我们有一个产品类，上级需要我们使用size过滤产品，现在我们来做产品的过滤类\npublic class ProductFilter { public static IEnumerable\u0026lt;Product\u0026gt; FilterBySize(IEnumerable\u0026lt;Product\u0026gt; products, Size size) { foreach(var p in products) if (p.Size == size) yield return p; } } 注意到，如果上级又需要根据color过滤产品，我们又需要打开ProductFilter类，添加FilterByColor，而如果上级又需要我们同时根据color和size过滤产品，我们还需要再添加FilterByColorAndSize。\n如果Product有更多属性，这意味着我们需要反复的修改ProductFilter类，而该类可能已经交付了。因此这是一个非常不好的设计。\n规格模式 # 首先我们定义两个接口\npublic interface ISpecification\u0026lt;in T\u0026gt; { bool IsSatisfied(T t); } public interface IFilter\u0026lt;T\u0026gt; { IEnumerable\u0026lt;T\u0026gt; Filter(IEnumerable\u0026lt;T\u0026gt; items, ISpecification\u0026lt;T\u0026gt; spec); } 可以理解成ISpecification处理t是否满足某个条件，而IFilter返回传入的items中所有满足传入的spec规格的项目。\n那么此时Filter只要这样写，之后便可以固定住不再开放修改了。\npublic class BetterFilter : IFilter\u0026lt;Product\u0026gt; { public IEnumerable\u0026lt;Product\u0026gt; Filter(IEnumerable\u0026lt;Product\u0026gt; items, ISpecification\u0026lt;Product\u0026gt; spec) { foreach (var i in items) { if (spec.IsSatisfied(i)) { yield return i; } } } } 而基础的规格类也很简单\npublic class SizeSpecification(Size size) : ISpecification\u0026lt;Product\u0026gt; { public bool IsSatisfied(Product t) =\u0026gt; t.Size == size; } public class ColorSpecification(Color color) : ISpecification\u0026lt;Product\u0026gt; { public bool IsSatisfied(Product t) =\u0026gt; t.Color == color; } 如果我们需要规格的组合\npublic class AndSpecification\u0026lt;T\u0026gt;(ISpecification\u0026lt;T\u0026gt; first, ISpecification\u0026lt;T\u0026gt; second) : ISpecification\u0026lt;T\u0026gt; { private ISpecification\u0026lt;T\u0026gt; _first = first ?? throw new ArgumentNullException(paramName: nameof(first)), _second = second ?? throw new ArgumentNullException(paramName: nameof(second)); public bool IsSatisfied(T t) { return _first.IsSatisfied(t) \u0026amp;\u0026amp; _second.IsSatisfied(t); } } 只需要用AndSpecification将不同的Specification结合到一起生成新的Specification即可。\n注意到开发过程中BetterFilter类完全不需要修改，只需要拓展地多写几个实现ISpecification的规格类就可以完成功能的扩展。\n","date":"27 August 2025","externalUrl":null,"permalink":"/posts/1759232882925-post5/","section":"Posts","summary":"","title":"OODP1 开闭原则(OCP)","type":"posts"},{"content":" 一个类应该仅有一个引起它变化的原因 # 实践上我遇到的问题是如何定义引起它变化的原因？是宏观的原因还是微观的原因？\n答案是宏观的 # Eric: 单一职责原则实践过程怎么把握，比如课程中说你要做一个Journal类来存入和查看日记，如果你又想要持久化它的话，不能直接在Journal类中写Save方法，而是要写一个新类Persistence来管理持久化。依据是一个类只能有一个引起他变化的原因。但是Journal类本身不也有存入和查看两个功能吗，两个功能变化时都要修改Journal类啊，这符合单一职责原则吗？难道要把存入和查看两个功能也分成两个类吗？具体实践中我们又要怎么把握呢？\nDeepseek的回答 # 1. 重新理解“引起变化的原因” # 单一职责原则（SRP）的核心不是“一个类只能有一个方法”，而是“一个类应该有且仅有一个引起它变化的原因”。\n这原因不是一个具体的功能，而是一个**高层级的、会变化的“轴”**或“关切点”。\nJournal类的职责：管理日记的核心业务逻辑（业务实体）。\n变化原因：当“日记”这个概念本身需要变化时，这个类才需要改变。 例子：我们需要为日记增加“标签”功能；我们需要修改日记的标题长度限制；我们需要在日记内容中支持Markdown格式。这些变化都围绕着“日记是什么、有什么行为”这个核心关切点。 “添加条目”和“查看条目” 都是对日记这个核心实体进行操作的核心行为。它们共享同一个变化原因——日记业务逻辑的变更。因此，它们属于同一个职责。 Persistence类的职责：处理数据的持久化机制（存储与读取）。\n变化原因：当数据存储的方式或位置发生变化时，这个类才需要改变。 例子：我们之前把日记保存到文本文件，现在想改成保存到MySQL数据库；我们想从本地存储改为云存储（如AWS S3）；我们想改变文件保存的格式（从JSON改为XML）。这些变化完全不影响Journal类本身是什么，它只关心“怎么存、存到哪里”。 2. 为什么不能把Save方法放在Journal里？ # 如果将Save方法直接放在Journal类中，就意味着这个类现在有两个引起它变化的原因：\n日记的业务逻辑变化（如增加标签）。 持久化机制的变化（如从文件存到数据库）。 这违反了SRP。带来的直接问题是：\n可维护性差：一个负责数据库连接的开发人员修改了Save方法，可能会意外影响到另一个只关心业务逻辑（如addEntry方法）的开发人员。 复用性差：如果你想重用Journal这个业务实体，但新的项目想用不同的存储方式（比如内存存储），你就必须把Journal类也拖过去，并附带一堆你可能不需要的文件I/O代码。 测试困难：测试Journal的业务逻辑时，你必须绕过或模拟（Mock）文件系统操作，使得单元测试变得复杂。 而通过引入Persistence类，你将变化隔离了。Journal的变更和Persistence的变更互不影响。你可以轻松地替换不同的持久化策略而不影响核心业务代码。\n3. 实践中如何把握这个“度”？（关键部分） # 这是一个设计权衡的艺术，没有绝对正确的答案，但有一些非常实用的指导原则：\na. 关注“变化轴”，而非功能数量 # 不要简单地数一个类有多少个方法。而要问自己：“哪些变化会迫使这个类被修改？”。如果所有方法都因为同一个宏观原因而变化，那它们很可能属于同一个职责。\nJournal的例子：addEntry和removeEntry都围绕着“管理条目生命周期”这个轴。 另一个例子：一个User类有login、logout、changePassword方法。它们都围绕着“用户身份认证”这个轴。但如果把“发送营销邮件sendPromotionalEmail”也塞进User类，那就不对了，因为那属于“通知”或“营销”这个不同的变化轴。 b. 应用“共同闭包原则”（CCP） # 这是包级别的一个原则，但同样适用于类设计：将那些同时、因同样原因而变化的东西放在一起。\nJournal里的方法总是因为日记业务的变化而一起改变。 Persistence类里的方法总是因为存储技术的变化而一起改变。 但它们两者之间几乎不会因为对方的原因而改变。所以它们应该被分开。 c. 从“角色”和“用户”视角思考 # 想象一下谁会要求这个类做出改变。\n产品经理可能会要求改变Journal：“我们需要给日记加个心情图标”。 运维工程师可能会要求改变Persistence：“数据库要从MySQL迁移到PostgreSQL，连接字符串和方言要改”。 如果来自完全不同角色、不同关切方的需求会迫使你修改同一个类，那么这个类很可能承担了多个职责。\nd. 避免过早优化和过度设计 # 在项目早期，如果持久化方式非常稳定（比如确定就用一个简单的JSON文件），并且项目很小，那么稍微违反一下SRP的代价可能低于引入一个新类的复杂度。一开始可以放在一起，但要有意识地将实现细节隐藏好（比如用private方法），并在第一次需要变化时（比如要加第二种存储方式）果断进行重构，将其提取到独立的类中。\n总结 # Journal类的“存入”和“查看”：属于对同一业务实体（日记）的操作，是同一个职责，变化原因相同，因此放在一个类是合理的，符合SRP。 Journal类的“Save”方法：属于另一个职责（持久化），变化原因不同（业务逻辑 vs. 存储技术），因此应该拆分成Persistence类。 实践心法： 聚焦于 “变化的原因” ，而不是方法的数量。 思考 “谁会要求它改变”（产品经理 vs. 运维）。 遵循 “共同闭包原则”——因同样原因而变化的东西放在一起。 循序渐进，在简单和设计之间权衡，在代码第一次发出“臭味”（难以修改、难以测试）时果断重构。 ","date":"27 August 2025","externalUrl":null,"permalink":"/posts/1759232878687-post4/","section":"Posts","summary":"","title":"OODP0 单一职责原则(SRP)","type":"posts"},{"content":" 背景 # 两个月前看了一点Effective Modern Cpp，后来听说作者Scott Mayer写了三本书，Cpp三部曲(E C, More E C, E Modern C)。于是搁置了Effective Modern Cpp，转而去看Effective Cpp。当时看到作者的一个观点是函数返回值优先const 即优先写\nconst std::string func()... 而不是\nstd::string func()... 原因是有人会在if语句里做判断，发生typo，将判断写成赋值 如\nMatrix64 a, b, c; ... if (a * b = c){ // should be \u0026#34;==\u0026#34; } 若*的重载函数返回const Matrix64，编译器就会报错，否则就难以找到错误发生的地方。 听起来非常有道理，于是书上说\n一个函数返回一个 constant value（常量值），常常可以在不放弃安全和效率的前提下尽可能减少客户的错误造成的影响。\n然而今天机缘巧合之下我去问dicksip我们是不是应该在cpp的函数返回值上默认const，它却给出了否定的答案。 其实原因也非常简单，只是我之前没有去想\n为什么我们不应该广泛使用const返回值? # 原因1 降低性能(阻止移动语义,RVO,NRVO) # 在现代cpp中，函数返回值const会影响移动语义，移动语义需要非常量右值 在没有const的情况下\nData getData(){ Data data; ... return data; } Data receiver = getData();// 触发移动语义，甚至RVO,NRVO // 如果 // const Data getData(); // 则只能触发拷贝。 原因2 阻碍现代范式 # 如\n链式调用(Fluent Interface) # class MyString{ public: MyString\u0026amp; toUpper(); } proccessString.toUpper();//没有const时才能写 就地操作 # void modifyValue(std::string\u0026amp;\u0026amp; str); modifyValue(getValue()) //高效接受右值处理 一种惯用法 # // 从流中读取数据并检查是否成功 if (std::cin \u0026gt;\u0026gt; value) { ... } // 在循环中获取锁并检查是否成功 while (std::unique_lock lock(mutex, std::try_to_lock)) { ... } // 获取一个资源指针并检查是否有效 if (auto ptr = acquireResource()) { ... } 这里unique_ptr和unique_lock等都必须接受非常量右值，如果const就无法构造。\n原因3 更好的现代方案 # 原先这种写法解决的问题在今天已经不是问题，现代编译器都可以对if(),while()等中出现的异常赋值操作发出Warning 我们没有必要为了有限的好处牺牲掉众多现代cpp的优秀特性，那显然是因噎废食。\n原因4 一致性原则 # cpp标准库没有那样做，我们应该与cpp标准库保持一致\n什么时候应该用const返回值? # 返回引用和指针时，如果不希望被修改内部数据，必须使用const返回值， 此处较为简单，省略\n总结 # 我不想说什么“我们应该在读书的时候多加思考”之类的废话。很多时候，我们就是没有办法想到那些东西，才会陷入歧途。所以与其说我们要多思考，不如讲我们可以从哪些方面思考。\n1. 考虑书的时代性 # Effective Cpp这本书是Scott Mayer三部曲中的第一本，初版于1991年。这个时间就注定这本书很难完全适用今天的C++23(甚至2c)时代，时过境迁，我们在计算机这样的火热领域，看到如此之大的年代差别时，就应该对书的内容保持较大的警惕了。\n2. 考虑自己的经验 # 虽然很多时候很多学科确实会冲击我们的经验，如离散数学，概率论，实分析中很多内容(比如0-1间有理数的测度为0)都和经验相悖，这些实实在在地让我们这样的工科大学生渐渐放弃了我们的经验。然而当理论和经验发生冲突时，进行一个彻底的探究，来搞清楚孰对孰错也是件很要紧的事。如果是经验错了，比起放弃经验和直觉，我们更重要的是培养正确的新的经验和直觉，对我们直觉所依赖的东西进行一个彻底的修复，将错的感觉清楚，正确的理论形成直觉，这才是求实的态度。\n当我们看到返回值应该const这样激进的言论的时候，我们要去质疑，毕竟值优先const是很常见的说法，而返回值const则很少见了。在我们平时阅读的开源代码中，将返回值const的写法也是极其罕见的。种种迹象表明，这一个item是极其有悖于常识的。\n3. 实践出真知 # 在足够多，足够严谨的工程实践中，我们会找到正确的答案，一切不合适的理论自然会不攻自破，所以多参与工程一定是好的。\n","date":"20 August 2025","externalUrl":null,"permalink":"/posts/1759232872971-post3/","section":"Posts","summary":"","title":"关于cpp中const返回值","type":"posts"},{"content":"","date":"15 July 2025","externalUrl":null,"permalink":"/tags/%E5%A4%8F%E5%87%A0%E6%8A%8A%E6%83%B3/","section":"Tags","summary":"","title":"夏几把想","type":"tags"},{"content":"","date":"15 July 2025","externalUrl":null,"permalink":"/categories/%E6%83%B3%E6%B3%95%E6%9E%84%E6%80%9D/","section":"Categories","summary":"","title":"想法构思","type":"categories"},{"content":"故事大概就是我突然想做一个todo类软件，在我和deepseek切磋了创新点，功能点等之后，deepseek给出了如下不明觉厉的项目规划说明书\n智能任务协同管理平台需求规格说明书 # 文档编号：TDMS-RS-2025-v1.2\n发布日期：2025年7月15日\n1. 系统概述 # 开发基于Qt/C++与SQLite/PostgreSQL的跨平台任务管理系统，集成AI增强能力，支持个人至团队级任务管理，实现任务规划→执行监控→知识沉淀→智能进化的全生命周期管理。\n2. 功能需求 # 2.1 核心任务管理模块 # 功能编号 需求描述 技术实现要点 TD-0100 多视图任务展示 QTabWidget + QStackedWidget - 看板视图（Kanban） QGraphicsView拖拽泳道 - 时间线视图（Gantt） QCustomPlot动态图表 - 列表视图/树状视图 QTreeView + QStandardItemModel TD-0200 任务元素管理 数据库关系设计 - 任务属性：标题/描述/优先级/标签 SQLite BLOB存储富文本 - 子任务分解（无限层级） 邻接表结构存储 - 附件管理（文档/图片/链接） QFileSystemWatcher监控目录 TD-0300 智能提醒系统 QCalendarWidget集成 - 时间提醒（绝对/相对时间） 基于cron表达式解析器 - 位置提醒（GPS触发） QtPositioning模块 - 设备状态提醒（空闲时推送） QSystemIdleDetector 2.2 协同工作模块 # 功能编号 需求描述 技术实现要点 CL-0100 团队空间管理 OAuth2.0认证体系 - 角色权限控制（所有者/编辑者/查看者） RBAC模型数据库设计 - 项目空间隔离 SQLite ATTACH DATABASE CL-0200 实时协作功能 WebSocket + Operational Transform - 任务分配与@提及通知 QNetworkSession管理连接 - 协同编辑冲突解决 CRDT算法实现 - 版本历史追溯（可回滚） SQLite时间旅行扩展 CL-0300 沟通集成 libcurl网络库 - 评论线程（Markdown支持） QTextDocument解析 - 邮件同步（任务变更通知） SMTP协议实现 2.3 AI增强模块 # 功能编号 需求描述 技术实现要点 AI-0100 自然语言任务解析 ONNX运行时本地推理 - 任务语义解构（提取实体/关系） 微调BERT模型（\u0026lt;50MB） - 自动生成子任务树 决策树算法生成 AI-0200 动态规划引擎 QtConcurrent并行计算 - 时间预估（基于历史效率分析） ARIMA时间序列预测 - 智能排程（资源冲突检测） 约束满足问题求解器 - 风险预警（关键路径标识） 图论算法应用 AI-0300 知识增强系统 DuckDB向量数据库 - 任务关联文档推荐 Sentence-BERT相似度匹配 - 代码片段自动生成（开发任务） OpenAI Codex本地化模型 2.4 分析预测模块 # 功能编号 需求描述 技术实现要点 AN-0100 生产力分析 QtCharts数据可视化 - 个人效能热力图（专注时段分析） QSensor数据采集 - 团队负载均衡图 力导向图算法 AN-0200 预测性报告 SQL窗口函数分析 - 项目完成概率预测 蒙特卡洛模拟 - 延期风险指数计算 逻辑回归模型 3. 非功能性需求 # 3.1 性能需求 # gantt title 关键性能指标 dateFormat YYYY-MM-DD section 响应时间 任务加载(1000条) ：active, p1, 2025-08-01, 1.2s AI解析响应(P95) ： p2, after p1, 0.8s 协同操作同步 ： p3, after p2, 0.3s section 资源占用 内存常驻 ：crit, 2025-08-01, 120MB 存储压缩比 ： \u0026gt;=5:1 3.2 安全需求 # 数据加密：SQLite页级AES-256加密（SQLCipher扩展） 隐私保护：AI处理本地化（可选关闭云同步） 审计追踪：操作日志HMAC-SHA256签名 3.3 可靠性需求 # 崩溃恢复：自动保存点（15分钟增量备份） 冲突解决：三阶段协同冲突处理策略： 客户端自动合并（基于CRDT） 用户决策界面（可视化差异对比） 版本分支存档 3.4 兼容性需求 # 平台 支持级别 技术方案 Windows 完整功能 MSVC 2022编译 macOS 完整功能+原生UI Qt for macOS Linux 完整功能 Wayland/X11双支持 Android 核心功能 Qt 6.5 Mobile模块 Web 只读视图 WebAssembly编译 4. 系统架构 # 4.1 逻辑架构 # +-----------------------+ | Presentation | | QWidgets / QtQuick | +----------+------------+ | (信号槽) +----------v------------+ | Business Logic | | - 任务管理核心 | | - AI推理引擎 | | - 协同控制器 | +----------+------------+ | (DAO接口) +----------v------------+ | Data Layer | | - SQLite OLTP | | - DuckDB OLAP | | - 模型文件存储 | +-----------------------+ 4.2 部署架构 # graph TD A[桌面客户端] --\u0026gt; B[本地数据库] A --\u0026gt; C[AI模型仓库] A \u0026lt;--\u0026gt; D[协同服务器] D --\u0026gt; E[(PostgreSQL集群)] D --\u0026gt; F[Redis实时队列] G[移动端] --\u0026gt; D H[Web端] --\u0026gt; D 5. AI集成方案 # 5.1 智能规划工作流 # sequenceDiagram participant User participant UI participant AI Engine participant Database User-\u0026gt;\u0026gt;UI： 输入“开发邮件客户端，需加密模块，两周完成” UI-\u0026gt;\u0026gt;AI Engine： 发送自然语言请求 AI Engine-\u0026gt;\u0026gt;AI Engine： 实体识别（技术栈/时间/功能） AI Engine-\u0026gt;\u0026gt;Database： 查询相似任务历史数据 Database--\u0026gt;\u0026gt;AI Engine： 返回历史执行指标 AI Engine-\u0026gt;\u0026gt;AI Engine： 生成任务分解树 AI Engine-\u0026gt;\u0026gt;UI： 返回结构化规划： • 子任务：UI框架/认证模块/加密集成 • 时间线：甘特图+风险提示 UI-\u0026gt;\u0026gt;User： 可视化展示可编辑方案 5.2 关键技术实现 # // AI任务解析核心类 class TaskParser : public QObject { Q_OBJECT public: explicit TaskParser(QObject *parent = nullptr); Q_INVOKABLE ParsedTask parseDescription(const QString \u0026amp;text); private: // ONNX推理引擎 Ort::Session *nlpSession; // 本地知识图谱 DuckDBConnection *knowledgeGraph; }; // 返回数据结构 struct ParsedTask { QString title; QVector\u0026lt;Subtask\u0026gt; subtasks; QMap\u0026lt;QString, QTime\u0026gt; timeEstimates; QVector\u0026lt;KnowledgeLink\u0026gt; relatedResources; }; 6. 交付物规划 # 6.1 阶段化交付 # 迭代 功能集 AI能力 周期 MVP 基础任务管理+本地AI解析 L1：关键词提取 8周 v1.0 团队协同+动态规划 L2：时间预估 12周 v1.5 预测分析+跨平台 L3：风险建模 6周 v2.0 AR集成+企业级扩展 L4：自适应工作流 10周 6.2 质量保障 # 自动化测试覆盖率： 核心逻辑：≥85%（QTest框架） UI交互：≥70%（Squish自动化） AI模型评估： 任务分解准确率：F1-score ≥0.82 时间预估误差：MAPE ≤15% 7. 创新价值矩阵 # quadrantChart title 功能价值分析 x-axis 开发成本 → y-axis 用户价值 ↑ quadrant-1 战略核心 quadrant-2 差异化优势 quadrant-3 基础必备 quadrant-4 优化选项 “动态AI规划”： [0.7, 0.95] “协同冲突解决”： [0.6, 0.8] “多视图看板”： [0.3, 0.9] “知识图谱”： [0.65, 0.7] “AR集成”： [0.85, 0.6] 本需求书完整涵盖传统任务管理核心功能（看板/协同/提醒）与AI创新点（智能规划/预测分析），建议采用模块化开发策略：\n基础框架搭建：实现TD-0100/TD-0200模块（4周） 协同核心开发：完成CL系列功能（6周） AI引擎集成：部署AI-0100/AI-0200（8周） 分析预测扩展：构建AN模块（4周） 具体内容我觉得非常好，所以打算朝这个方向做，虽然很明显我做不成很厉害的样子但至少可以做着玩玩。\n起手 # 我肯定不愿意用qt原生的组件，因为太丑，自己搓组件又太麻烦，所以我干脆上网找开源项目。有一个QFluentWidget的项目，但是它的cpp的版本要几千块钱买，所以我只好放弃。后面找到了ElaWidgetTools，今天下午刚配好ElaWidgetTools，预计后面以ElaWidgetTools作为主要UI库。 目前项目刚刚开始，希望我不要又做成一坨。\n","date":"15 July 2025","externalUrl":null,"permalink":"/posts/1759232853936-post1/","section":"Posts","summary":"","title":"构想一个智能todo项目进度管理软件","type":"posts"},{"content":"","date":"15 July 2025","externalUrl":null,"permalink":"/tags/%E6%B2%A1%E6%9C%89%E5%AE%8C%E6%88%90/","section":"Tags","summary":"","title":"没有完成","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]