Jack N @ GitHub

Full stack AI Engineer, focus on: React, Next.js, node.js and .Net

1. 题目

设计并实现一个 LRU(Least Recently Used,最近最少使用)缓存,支持 get 和 put 操作,要求两者都是 O(1) 时间复杂度。

  • 构造时给定正整数 capacity 表示缓存容量。
  • get(key):存在则返回 value,否则返回 -1。
  • put(key, value):写入数据。若写入导致容量超限,应驱逐最久未使用的键。
  • 关键:get 和 put 都要更新「使用顺序」,被访问的键变成「最近使用」。

示例:

1
2
3
4
5
6
7
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1,此时 1 变为最新,2 变为最旧
cache.put(3, 3); // 容量满,驱逐最旧的 2
cache.get(2); // 返回 -1(未找到)
cache.get(3); // 返回 3

2. 解题思路

LRU 的两大需求互相矛盾:

  1. O(1) 按 key 查找 —— 需要哈希表。
  2. O(1) 维护使用顺序、淘汰尾部 —— 需要能在头部/尾部任意增删的有序结构。

答案:哈希表 + 双向链表。这正是浏览器缓存、Redis allkeys-lru、各类内存缓存的标准实现。

  • 双向链表:越靠近 head 越「新」,越靠近 tail 越「旧」。淘汰时直接删 tail 前一个。
  • 哈希表:key -> 链表节点,实现 O(1) 定位到某个节点。
  • 命中或写入后,把该节点用 addToHead(先 remove 再插入头部)移到最前。

为什么是双向而非单向?因为删除任意节点时需要 O(1) 拿到它的前驱,单向链表做不到。

巧用语言特性:JavaScript 的 Map 本身就维护插入顺序,把 key 重新删除再插入等价于「移到最新」,可以极简实现。面试时建议两种都讲:Map 版展示语言理解,手写双向链表版展示数据结构功底。

3. TypeScript 实现

3.1 极简版:利用 Map 的插入有序性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LRUCache {
private map = new Map<number, number>();
constructor(private capacity: number) {}

get(key: number): number {
if (!this.map.has(key)) return -1;
const value = this.map.get(key)!;
// 删除后重新插入 = 移动到最新(Map 尾部为最近使用)
this.map.delete(key);
this.map.set(key, value);
return value;
}

put(key: number, value: number): void {
if (this.map.has(key)) {
this.map.delete(key);
} else if (this.map.size >= this.capacity) {
// 删除第一个 key,即最久未使用(队首)
const oldest = this.map.keys().next().value!;
this.map.delete(oldest);
}
this.map.set(key, value);
}
}

3.2 经典版:哈希表 + 手写双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
interface DLinkedNode {
key: number;
value: number;
prev: DLinkedNode | null;
next: DLinkedNode | null;
}

class LRUCacheAdvanced {
private map = new Map<number, DLinkedNode>();
private size = 0;
// 哨兵节点,避免大量 null 判断
private head: DLinkedNode = { key: 0, value: 0, prev: null, next: null };
private tail: DLinkedNode = { key: 0, value: 0, prev: null, next: null };

constructor(private capacity: number) {
this.head.next = this.tail;
this.tail.prev = this.head;
}

get(key: number): number {
const node = this.map.get(key);
if (!node) return -1;
this.moveToHead(node); // 命中即刷新为最新
return node.value;
}

put(key: number, value: number): void {
const node = this.map.get(key);
if (node) {
node.value = value;
this.moveToHead(node);
return;
}

const newNode: DLinkedNode = { key, value, prev: null, next: null };
this.map.set(key, newNode);
this.addToHead(newNode);
this.size++;

if (this.size > this.capacity) {
const removed = this.removeTail(); // 淘汰最旧
this.map.delete(removed.key);
this.size--;
}
}

private addToHead(node: DLinkedNode): void {
node.prev = this.head;
node.next = this.head.next;
this.head.next!.prev = node;
this.head.next = node;
}

private removeNode(node: DLinkedNode): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}

private moveToHead(node: DLinkedNode): void {
this.removeNode(node);
this.addToHead(node);
}

private removeTail(): DLinkedNode {
const node = this.tail.prev!;
this.removeNode(node);
return node;
}
}
  • 时间复杂度:get / put 均为 O(1)。
  • 空间复杂度:O(capacity)。

4. 面试延伸

  • LFU 缓存(LeetCode 460) 是 LRU 的进阶,需要频率桶 + 双向链表,更烧脑。
  • 真实工程:Redis 用近似 LRU(随机采样淘汰)省内存;前端 keep-alive、图片缓存、请求缓存都用 LRU 思想。
  • 讲题要点:先说清「为什么单结构做不到 O(1)」,再自然引出「哈希 + 双向链表」的组合,展示的是权衡能力而非背代码。

难度:中等 | LeetCode 146 题 | 全栈数据结构压轴题

1. 题目

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

1
2
3
4
5
输入: 1 -> 2 -> 4 , 1 -> 3 -> 4
输出: 1 -> 1 -> 2 -> 3 -> 4 -> 4

输入: l1 = [], l2 = []
输出: []

节点定义:

1
2
3
4
5
6
7
8
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}

2. 解题思路

经典的**归并(merge)**操作,也是归并排序的核心子步骤。

引入一个哑节点 dummy 作为结果链表的哨兵头,避免讨论「第一个节点是谁」的边界。再用 curr 指向结果链表尾部:

  1. 同时遍历 l1、l2,比较当前值,把较小的接到 curr.next,对应链表指针前移。
  2. curr 也前移一位。
  3. 有一方走完,把另一方剩余部分整段接上(它本身已有序)。
  4. 返回 dummy.next。
  • 时间复杂度:O(m + n),每个节点处理一次。
  • 空间复杂度:O(1),只重排指针,不新建节点。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function mergeTwoLists(
l1: ListNode | null,
l2: ListNode | null
): ListNode | null {
const dummy = new ListNode(-1);
let curr = dummy;

while (l1 !== null && l2 !== null) {
if (l1.val <= l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}

// 接上剩余部分(最多一条链表还有货)
curr.next = l1 !== null ? l1 : l2;

return dummy.next;
}

// 递归写法(更短,栈空间 O(m+n))
function mergeTwoListsRecursive(
l1: ListNode | null,
l2: ListNode | null
): ListNode | null {
if (l1 === null) return l2;
if (l2 === null) return l1;

if (l1.val <= l2.val) {
l1.next = mergeTwoListsRecursive(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoListsRecursive(l1, l2.next);
return l2;
}
}

4. 面试延伸

  • 合并 K 个有序链表(LeetCode 23) 是本题升级,可用「最小堆」或「分治两两合并」,复杂度 O(N log k)。
  • dummy 哑节点是链表题的通用技巧,几乎能消除所有头节点特判,务必形成肌肉记忆。
  • 稳定性:比较时用 <= 保证相等时优先取 l1,这正是归并排序「稳定」的来源。

难度:简单 | LeetCode 21 题 | 归并思想入门

1. 题目

给你单链表的头节点 head,请你反转链表,并返回反转后的链表头。

示例:

1
2
3
4
5
输入: 1 -> 2 -> 3 -> 4 -> 5
输出: 5 -> 4 -> 3 -> 2 -> 1

输入: head = []
输出: []

链表节点定义:

1
2
3
4
5
6
7
8
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}

2. 解题思路

2.1 迭代(首选)

反转的本质是把每个节点的 next 指针掉头,指向它原来的前驱。用三个指针:

  • prev:已反转部分的头,初始 null。
  • curr:当前正在处理节点,初始 head。
  • nextTemp:在处理前先保存 curr.next,否则断链后找不到后继。

每一步:暂存后继 → curr.next = prev → prev 前移到 curr → curr 前移到暂存的后继。循环结束时 prev 就是新头。

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

2.2 递归

递归到链表末尾,回溯时把后一个节点的 next 指回自己:head.next.next = head,再断开 head.next = null 防止成环。简洁但栈空间 O(n)。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 迭代
function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null;
let curr = head;

while (curr !== null) {
const nextTemp = curr.next; // 1. 暂存后继
curr.next = prev; // 2. 反转指针
prev = curr; // 3. prev 前进
curr = nextTemp; // 4. curr 前进
}

return prev;
}

// 递归
function reverseListRecursive(head: ListNode | null): ListNode | null {
if (head === null || head.next === null) return head;

const newHead = reverseListRecursive(head.next);
head.next.next = head; // 让后继指回自己
head.next = null; // 断开原指针,避免环
return newHead;
}

4. 面试延伸

  • 反转区间(LeetCode 92 反转链表 II) 或 每 k 个一组翻转(25 题) 是高频进阶,核心仍是本节的指针操作,外加「哑节点 dummy」简化边界。
  • 判断回文链表(234):快慢指针找中点 + 反转后半段 + 逐一比较,空间 O(1)。
  • 递归版本要能徒手画图讲清 head.next.next = head 的含义,这是面试官最爱追问的点。

难度:简单 | LeetCode 206 题 | 链表题地基

1. 题目

给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

注意「子串」是连续的,区别于「子序列」。

示例:

1
2
3
4
5
6
7
8
9
10
输入: s = "abcabcbb"
输出: 3
解释: 无重复字符的最长子串是 "abc",长度为 3。

输入: s = "bbbbb"
输出: 1

输入: s = "pwwkew"
输出: 3
解释: 最长子串是 "wke",注意 "pwke" 是子序列不是子串。

2. 解题思路

标准的滑动窗口问题。维护一个窗口 [left, right],保证窗口内始终无重复字符。

用 Map<字符, 最近一次出现的下标> 记录每个字符最后出现的位置。右指针 right 逐个字符向右扩展:

  • 如果 s[right] 之前出现过,且其位置在窗口内(>= left),说明加入它会重复,于是把 left 直接跳到「重复字符上次位置 + 1」,一步到位,无需逐个收缩。
  • 更新 s[right] 的位置为 right。
  • 每一步用 right - left + 1 更新最大长度。

关键技巧:left 只能右移不能回退,所以要取 Math.max(left, lastPos + 1),避免像 abba 这类用例把 left 拉回左边。

  • 时间复杂度:O(n),每个字符最多被访问一次。
  • 空间复杂度:O(min(n, 字符集大小))。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function lengthOfLongestSubstring(s: string): number {
const lastPos = new Map<string, number>();
let left = 0;
let maxLen = 0;

for (let right = 0; right < s.length; right++) {
const ch = s[right];

// 字符在窗口内出现过,left 跳到重复处之后
if (lastPos.has(ch)) {
left = Math.max(left, lastPos.get(ch)! + 1);
}

lastPos.set(ch, right);
maxLen = Math.max(maxLen, right - left + 1);
}

return maxLen;
}

// 测试
console.log(lengthOfLongestSubstring("abcabcbb")); // 3
console.log(lengthOfLongestSubstring("bbbbb")); // 1
console.log(lengthOfLongestSubstring("pwwkew")); // 3
console.log(lengthOfLongestSubstring("abba")); // 2

4. 面试延伸

  • 不用 Map 的写法? 字符集有限(如 ASCII)时用长度为 128 的数组更快;也可用 Set 记录窗口内字符,重复时循环删除 s[left++] 直到不重复。
  • 变形题:至多包含两个不同字符的最长子串(LeetCode 340)、最小覆盖子串(76) 都是滑动窗口的进阶,套路一致:右扩满足条件、左缩优化答案。
  • 记住模板:for right + while 违反条件 then left++,能覆盖绝大多数子串类题目。

难度:中等 | LeetCode 3 题 | 滑动窗口入门必刷

概述

HTML/URL中有2个概念,encode和escape,乍一看差不多, 都是用于处理特殊字符(保留字符),其实用于不同场合:

  • encode: 主要针对URL地址中特殊字符(保留字符)的处理, 如针对 &, ?, =等字符
  • escape: 主要针对页面中的特殊字符(保留字符)的处理, 如针对 <, >, ", '字符

Escape

Escape/unescape 没有现成的函数,只能自己实现。实现也很简单,就是对字符串全文替换。

1
2
3
4
5
6
7
8
function escapeHTML(str) {
return str.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

Encode/Decode

Javascript 中,针对URL的处理是有通用方法的,encodeURIComponent 和encodeURI,酌情选择所需的函数。

  • encodeURI方法不会对下列字符编码: ASCII字母 数字 ~!@#$&*()=:/,;?+’
  • encodeURIComponent方法不会对下列字符编码: ASCII字母 数字 ~!*()’
  • 所以encodeURIComponent比encodeURI编码的范围更大。实际例子来说,encodeURIComponent会把 http:// 编码成 http%3A%2F%2F 而encodeURI却不会。

1. 题目

给你一个整数数组 nums,找出所有满足 nums[i] + nums[j] + nums[k] == 0 的三元组 [i, j, k],且要求:

  • i、j、k 两两不同。
  • 返回的三元组不能重复。

示例:

1
2
3
输入: nums = [-1, 0, 1, 2, -1, -4]
输出: [[-1, -1, 2], [-1, 0, 1]]
解释: 满足条件的不重复三元组如上,注意 [-1, 0, 1] 和 [0, 1, -1] 视为同一个。

2. 解题思路

暴力三重循环是 O(n³) 且难以去重。经典做法是排序 + 双指针,把复杂度降到 O(n²)。

  1. 排序:nums.sort((a, b) => a - b),让双指针成为可能,也让重复值相邻便于跳过。
  2. 固定一个数:外层枚举 nums[i],把问题转化为「在 i 右侧找两数之和等于 -nums[i]」——退化成 [两数之和 II(有序)]。
  3. 双指针夹逼:left = i + 1,right = n - 1:
    • sum === 0 → 记录答案,然后左右指针同时向内移动并跳过重复。
    • sum < 0 → 需要更大的数,left++。
    • sum > 0 → 需要更小的数,right--。
  4. 三处去重:
    • i 与前一个相同则跳过(避免重复的第一个数)。
    • 命中答案后,left、right 各自跳过相同值。

优化剪枝:排序后若 nums[i] > 0,三数之和不可能为 0,直接结束。

  • 时间复杂度:O(n²)。
  • 空间复杂度:O(log n)(排序栈空间,不计输出)。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function threeSum(nums: number[]): number[][] {
const res: number[][] = [];
const n = nums.length;
nums.sort((a, b) => a - b);

for (let i = 0; i < n - 2; i++) {
// 剪枝:最小值已大于 0,不可能凑出 0
if (nums[i] > 0) break;
// 去重:跳过相同的第一个数
if (i > 0 && nums[i] === nums[i - 1]) continue;

let left = i + 1;
let right = n - 1;

while (left < right) {
const sum = nums[i] + nums[left] + nums[right];

if (sum === 0) {
res.push([nums[i], nums[left], nums[right]]);
// 去重:跳过第二个数、第三个数的重复值
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}

return res;
}

// 测试
console.log(threeSum([-1, 0, 1, 2, -1, -4]));
// [[-1, -1, 2], [-1, 0, 1]]
console.log(threeSum([0, 0, 0, 0])); // [[0, 0, 0]]

4. 面试延伸

  • 扩展到 k-Sum? 排序后递归地固定 k-2 个数,最后两数用双指针,通用模板时间 O(n^{k-1})。
  • 最接近的三数之和(LeetCode 16)? 同样双指针,只是记录过程中 |sum - target| 的最小值。
  • 去重是最大坑点:一定要在push之后跳过重复,而不是用 Set 序列化数组去重(后者能过但脏)。

难度:中等 | LeetCode 15 题 | 双指针标杆题

1. 题目

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。

要求:

  • 每种输入只会对应一个答案(保证有且仅有一组解)。
  • 同一个元素不能使用两遍。
  • 可以按任意顺序返回答案。

示例:

1
2
3
输入: nums = [2, 7, 11, 15], target = 9
输出: [0, 1]
解释: nums[0] + nums[1] == 9,返回 [0, 1]

2. 解题思路

2.1 暴力法(不推荐)

两层循环枚举所有 (i, j) 组合,判断 nums[i] + nums[j] === target。

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)

面试时可以作为引子,但要立刻说出瓶颈在于「查找另一个数是否存在」这一步是线性的。

2.2 哈希表(最优)

核心转换:对于当前数 x,我们真正想知道的是「数组里有没有 target - x」。

遍历数组时,用 Map 记录「已经看过的数 -> 下标」。对每个 nums[i]:

  1. 计算补数 complement = target - nums[i]。
  2. 若 complement 已在 Map 中,直接返回 [map.get(complement), i]。
  3. 否则把 nums[i] -> i 存进 Map,继续。

因为「先查后存」,天然避免了同一个元素被使用两次。

  • 时间复杂度:O(n),只需一次遍历。
  • 空间复杂度:O(n),最坏存 n 个数。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function twoSum(nums: number[], target: number): number[] {
const seen = new Map<number, number>();

for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];

if (seen.has(complement)) {
return [seen.get(complement)!, i];
}

seen.set(nums[i], i);
}

// 题目保证有解,理论上不会走到这里
return [];
}

// 测试
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]
console.log(twoSum([3, 2, 4], 6)); // [1, 2]
console.log(twoSum([3, 3], 6)); // [0, 1]

4. 面试延伸

  • 为什么用 Map 而不是 Object? 数字做 key 时 Map 无需字符串化,也避免原型链上的键冲突,查找语义更清晰。
  • 如果数组已排序呢? 可用「双指针」左右夹逼,空间降到 O(1),这也是 [三数之和] 的基础。
  • 返回所有不重复组合? 那就需要排序 + 去重,思路升级为 k-Sum 通用解法。

难度:简单 | LeetCode 1 题 | 高频面试开场题

1. 概述

位操作是程序设计中对位模式按位或二进制数的一元和二元操作. 在许多古老的微处理器上, 位运算比加减运算略快, 通常位运算比乘除法运算要快很多. 在现代架构中, 情况并非如此:位运算的运算速度通常与加法运算相同(仍然快于乘法运算)。

2. 取反(NOT)~

3. 按位或(OR)|

4. 按位异或(XOR)^

5. 按位与(AND)&

6. 移位 >>, <<, >>>

  • 移动: >>, <<
  • 无符号右移: >>>
  • 无符号左移是不存在的,因为左右在低位补位,而低位没有正负数的概念,因此不存在无符号左移

7. 其他位操作技巧P

7.1. n & (n - 1)

该位运算技巧可以直接将 n 的二进制表示的最低位 1 移除。

  1. 不停的进行 n = n & (n - 1) 操作,直至n为0,可以计算二进制数的1的个数

环境:

  • .NET 5
  • ASP.NET Core MVC

1. .NET 5

作为一个资深.NET工程师,说句实话,.NET没落了,在国内更加的没落。之前做过8年node.js/前端,现如今又转回了.NET。也希望.NET能有更好的前景吧。个人觉得.NET还是有一定的市场的(当然,国内算小众了),有几点原因:

  1. .NET5开始,真正做到的跨平台(之前叫.NET CORE), 而且把那个’CORE’字也拿掉就,就叫.NET5, 可见微软的宏大目标;
  2. 开发效率,.NET软件开发,不管是web还是winForm,效率确实高
  3. 跨平台,从.net core开始,终于跨平台了….
  4. 性能大大提升。 .NET core性能大幅提升,尤其是跑在Linux上的时候;
  5. 有些已有的基于.NET的产品,还需要继续维护、开发,不会换个语言重写;
  6. 微软、以及和微软紧密合作的公司,也提供了不少.NET的应用场景;

工欲善其事必先利其器,既然要接着用.NET,那么下文将列一下开发必备工具,供参考。

2. IDE

2.1. Visual Studio

这个没的说,而且要用最新版的VS!

2.2. Visual Studio Code

微软的开源软件,同时是当前最流行的Web前端开发IDE。

作为备选,支持C#开发,对于简单的代码,或者测试一些语法、看代码什么的,这个足够,而且占用资源很少。

推荐插件:

  • Code Spell Checker, 检查代码中单词拼写错误。声明个变量,后来发现单词拼写错了,尴尬了,使用这个插件,即时提示拼写错误
  • C#, 安装此插件支持C#项目
  • GitLens,如果使用git,一定要装这个,方便查询提交历史,某一行上次代码的提交人、提交备注等;
  • Markdown All in One, markdown文件的插件,很强大,自动生成目录、编号等等。

3. 代码比较

3.1. Beyond Compare

收费软件,但功能确实是强大。

3.2. WinDiff

免费版,将就着用;

4. 图片处理

4.1. paint.net

(这个也是基于.NET开发的哦,因为我们机器必然有.net framework,所以这是还是很小的);
免费版,功能也很强大,占用资源很少。对于非专业的美工,足够用了!

5. 其他Web工具

5.1. node.js/npm

做前端web开发,不管是否用node.js,环境还是要有的;

5.2. API 测试 - Postman

方便测试API,免费版足够用了,支持账号同步,导入导出等等;

5.3. Color Pick

快速精确地从任何地方取色。

6. 其他工具

6.1. Linux客户端:MobaXterm

还在用putty? 试试MobaXterm吧,免费版支持保存20个session(节点、机器),足够用了;

  1. 集成了sftp,直接拖拽上传、下载文件
  2. 自带windows编辑器,不用vi了
  3. 多窗口模式
  4. 很好很强大… …

6.2. 远程连接: Remote Desktop Connection Manager

如果有大量的虚机、PC等需要远程连接使用,那这个工具就是很必要的。
微软的工具,虽说没什么更新了,但是很实用:

  1. 分组管理、保存用户、密码,支持继承用户名、密码(实验室机器一般用户名、密码都是一样的吧)

RQ

ASP.NET Core MVC 入门到精通 - 3. 使用MediatR

环境:

  • .NET 5
  • ASP.NET Core MVC (project)

1. MediatR

MediatR .NET中的简单中介者模式实现,一种进程内消息传递机制(无其他外部依赖)。支持以同步或异步的形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过C#泛型支持消息的智能调度。

Simple mediator implementation in .NET
In-process messaging with no dependencies.
Supports request/response, commands, queries, notifications and events, synchronous and async with intelligent dispatching via C# generic variance.

另:中介者模式 - 定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

2. 安装 & 配置

对于.NET5 (.net core), 使用nuget 安装MediatR.Extensions.Microsoft.DependencyInjection.

配置:

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddMediatR(typeof(Startup));
}

3. MediatR消息类型

3.1. Notifications 通知模式

Notifications 通知模式用于生产者发送通知,消费者(可以多个)接收到通知后,进行后续处理。
例:一个APS.NET 页面,访问时,发送Notifications通知;消费者简单记录收到通知的时间。

3.1.1. 定义基于INotification的通知类

1
public class Ping : INotification { }

3.1.2. 定义消费者(关注通知的处理方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pong1 : INotificationHandler<Ping>
{
public Task Handle(Ping notification, CancellationToken cancellationToken)
{
Debug.WriteLine($"Pong1, {DateTime.Now}");
return Task.CompletedTask;
}
}

public class Pong2 : INotificationHandler<Ping>
{
public Task Handle(Ping notification, CancellationToken cancellationToken)
{
Debug.WriteLine($"Pong2, {DateTime.Now}");
return Task.CompletedTask;
}
}

3.1.3. 发送消息通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基于dotnet core的依赖注入,注入IMediator对象
private readonly IMediator _mediator;
public HomeController(ILogger<HomeController> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}


public async Task<IActionResult> IndexAsync()
{
// e.g. 访问首页时,发送通知
await _mediator.Publish(new Ping());
return View();
}

3.1.4. 输出

1
2
3
Pong1, 5/27/2021 4:37:18 PM
Pong2, 5/27/2021 4:37:18 PM

3.2. Request/Response 请求响应模式

request/response用于命令和查询的场景。

3.2.1. 创建请求类:

1
2
3
public class RequestModel: IRequest<string>
{
}

3.2.2. 创建请求处理类

不同于通知模式,request/response只能有一个请求处理。

1
2
3
4
5
6
7
public class RequestHandeler : IRequestHandler<RequestModel, string>
{
public Task<string> Handle(RequestModel request, CancellationToken cancellationToken)
{
return Task.FromResult($"Pong {DateTime.Now}"); // 测试,返回内容给request
}
}

3.2.3. 页面中发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private readonly ILogger<HomeController> _logger;
private readonly IMediator _mediator;

public HomeController(ILogger<HomeController> logger, IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}

public async Task<IActionResult> IndexAsync()
{
// send request, and show Response
var response = await _mediator.Send(new RequestModel());
Debug.WriteLine("Got response in controller: " +response);

return View();
}

3.2.4. 输出

1
Got response in controller: Pong 5/28/2021 2:04:26 PM

4. 总结

  • MediatR是一种进程内消息传递机制。
  • 支持以同步或异步的形式进行请求/响应,命令,查询(CQRS),通知和事件的消息传递,并通过C#泛型支持消息的智能调度。
  • 其核心是消息的解耦。
  • 应用场景: 实现CQRS、EventBus等。

5. 参考 & 代码

ASP.NET Core MVC 入门到精通 - 3. 使用MediatR

RQ

0%