2024年9月

经过前面Redis入门系列三篇文章学习,相信大家已经准备好学习新知识了,到这里也算是真正开始学习Redis了。学习了软件安装,客户端选择,那么接下来也应该来了解Redis有什么,能干什么。

我们在第一章中就说过,Redis支持丰富的数据类型,今天我们就来学习Redis五种基础类型:字符串(String)、集合(Set)、有序集合(Sorted Set)、列表(List)、哈希(Hash)。

01
、字符串(String)

Redis中字符串类型是二进制安全的数据类型。可以把字符串理解成一个字符数组,这个数组里存放着很多特定编码的字符,因此这种设计,所有Redis中的字符串可以存储认识数据类型:整数、小数、字符串、图片、序列化对象、二进制数据等。

我们简单讲解几个最常见指令。

1.设置指定key的值,语法:
set key value

2.获取指定key的值,语法:
get key

3.删除指定key,语法:
del key

当然字符串还有很多其他指令,这里就不一一列举了,有兴趣的可以自己试试。

02
、集合(Set)

Redis中的集合类型可以理解为存放着一组无序的、无重复的元素的合集。你可以对元素进行增删查,也可以进行差集、交集、并集运算。

我们简单讲解几个最常见指令。

1.向指定key集合添加一个或多个元素,语法:
sadd key value1 value2…

2.获取指定key集合中所有元素,语法:
smembers key

3.删除指定key集合中的一个或多个元素,语法:
srem key value1 value2…

当然集合还有很多其他指令,这里就不一一列举了,有兴趣的可以自己试试。

03
、有序集合(Sorted Set)

Redis中的有序集合类型可以理解为集合类型+有序,即每个元素都对应一个分值,因此集合类型有的功能,有序集合类型基本也都有,同时还多了对分值进行聚合、筛选、排序等功能。

我们简单讲解几个最常见指令。

1.向指定key有序集合添加一对或多对元素及其分值,语法:
zadd key score1 value1 score2 value2…

2.获取指定key有序集合中指定元素的分值,语法:

3.删除指定key有序集合中指定元素,语法:
zrem key value

当然有序集合还有很多其他指令,这里就不一一列举了,有兴趣的可以自己试试。

04
、列表(List)

Redis中的列表类型是一个严格按照元素先后插入的顺序排列的字符串集合,并且可以通过在这个集合的两端进行插入和移除操作,还可以通过元素值或索引进行查找元素或移除元素。

我们简单讲解几个最常见指令。

1.从左边向指定key列表插入一个或多个元素,语法:
lpush key value1 value2 value3

2.从右边移除并获取指定key列表的第一个元素,语法:
rpop key

当然列表还有很多其他指令,这里就不一一列举了,有兴趣的可以自己试试。

05
、哈希(Hash)

Redis中的哈希类型可以理解成是一组键值对集合,键表示一个字符串字段,值表示数据对象,并且支持添加、获取或删除单个项即键值对,也可以获取整个哈希集合等功能。

我们简单讲解几个最常见指令。

1.向指定key哈希中添加一对或多对键值对,语法:
hset key field1 value1 field2 value2

2.获取指定key哈希中指定键对应的值,语法:
hget key filed

当然哈希还有很多其他指令,这里就不一一列举了,有兴趣的可以自己试试。

当然Redis不止这五种数据类型,还有其他更高级的数据类型,我们作为入门级教程,还是先掌握好这五大基本类型。只有掌握好了这些基础知识,只能Redis有什么,能做什么,才好在项目上熟练使用Redis,才好用Redis来解决各种复杂问题。

万丈高楼平地起,打好基础最重要,因此文章中没有列举到的指令也需要大家自己多去试试,亲自感受一下,才能更好的理解、记住、掌握。


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

前言

在欧阳的上一篇
这应该是全网最详细的Vue3.5版本解读
文章中有不少同学对Vue3.5新增的
onWatcherCleanup
有点疑惑,这个新增的API好像和
watch API
回调的第三个参数
onCleanup
功能好像重复了。今天这篇文章来讲讲新增的
onWatcherCleanup
函数的使用场景:
封装一个自动cancel的fetch函数

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

watch回调的第三个参数onCleanup

有些同学可能还不清楚
watch
回调的第三个参数
onCleanup
,我们先来看个demo,代码如下:

watch(id, (value, oldValue, onCleanup) => {
  console.log("do something");
  onCleanup(() => {
    console.log("cleanup");
  });
});

watch
回调的前两个参数大家应该很熟悉,分别是
value
新的值,
oldValue
旧的值。

第三个参数
onCleanup
大家平时可能用的不多,这是一个回调函数,当
watch
的值改变后或者组件销毁前就会执行
onCleanup
传入的回调。

在上面的demo中就是变量
id
改变时会触发
onCleanup
中的回调,进而
console
打印
"cleanup"
字符串。又或者所在的组件销毁前也会触发
onCleanup
中的回调,进而
console
打印
"cleanup"
字符串。

那我们在
onCleanup
中可以干嘛呢?

答案是可以清理副作用,比如在watch中使用
setInterval
初始化一个定时器。那么我们就可以在
onCleanup
的回调中清理掉定时器,无需去组件的
beforeUnmount
钩子函数去统一清理。

onWatcherCleanup
函数

onWatcherCleanup
函数的作用和
watch
回调的第三个参数
onCleanup
差不多,也是当
watch
的值改变后或者组件销毁前就会执行
onWatcherCleanup
传入的回调。

使用方法也很简单,代码如下:

import { watch, onWatcherCleanup } from "vue";

watch(id, () => {
  console.log("do something");
  onWatcherCleanup(() => {
    console.log("cleanup");
  });
});

从上面的代码可以看到
onWatcherCleanup
的用法其实和
watch
回调的第三个参数
onCleanup
差不多,区别在于这里的
onWatcherCleanup
是从vue中import导入的。

除了从vue中import导入的区别以外,还有一个区别是
onWatcherCleanup
不光在
watch
中可以使用,在
watchEffect
中同样也可以使用。比如下面这样的:

watchEffect(() => {
  console.log("do something in watchEffect", id.value);
  onWatcherCleanup(() => {
    console.log("cleanup watchEffect");
  });
});

和前面的例子一样,上面的代码中
id
的值改变后或者组件销毁时也会执行
onWatcherCleanup
函数中的
console.log
打印。

onWatcherCleanup
函数是从vue中import导入的,那么这意味着
onWatcherCleanup
函数的调用可以写在任意地方,只要最终经过函数的层层调用后还是在
watch
或者
watchEffect
的回调中就可以。

利用上面的这一特点我们可以使用
onWatcherCleanup
做到一些
onCleanup
做不到的事情,比如:封装一个自动
cancel

fetch
函数。

封装自动cancel的fetch函数

在讲这个之前我们先来了解一下如何
cancel
一个
fetch
函数。

这里涉及到
AbortController
接口,
AbortController
接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。

下面这个是
cancel
取消一个请求的demo,代码如下:

const controller = new AbortController();
const res = await fetch(url, {
  ...options,
  signal: controller.signal,
});

setTimeout(() => {
  controller.abort();
}, 500);

首先使用
new AbortController()
创建一个控制器对象
controller

其中的
controller.signal
返回一个
AbortSignal
对象实例,可以用它来和异步操作进行通信或者中止这个操作。

在我们这里把
controller.signal
作为
signal
选项直接传给fetch函数就可以了。

最后就是可以使用
controller.abort()
将fetch请求取消掉,在上面的demo中是如果超过500ms请求还没完成,那么就执行
controller.abort()
将fetch请求取消掉。

有了前面的知识铺垫,我们先来看看使用“自动
cancel

fetch
函数”的地方,代码如下:

<script setup lang="ts">
import { watch, ref, watchEffect, onWatcherCleanup } from "vue";
import myFetch from "./myFetch";

const id = ref(1);
const data = ref(null);

watch(id, async () => {
  const res = await myFetch(`http://localhost:3000/api/${id.value}`, {
    method: "GET",
  });
  console.log(res);
  data.value = res;
});
</script>

<template>
  <p>data is: {{ data }}</p>
  <button @click="id++">id++</button>
</template>

在上面的例子中使用
watch
监听了变量
id
,在监听的回调中会使用封装的
myFetch
函数请求接口。

上面的例子大家平时应该经常遇到,如果
id
的值变化很快,但是服务端接口请求需要2秒才能完成,这时我们期望只有最后一次
id
的值改变触发的请求才需要完成,其他请求都cancel取消掉。

如果在
myFetch
请求的过程中组件被销毁了,此时我们也期望能够将请求cancel取消掉。

在Vue3.5之前想要去实现上面的这两个需求很麻烦,但是有了Vue3.5的
onWatcherCleanup
函数后就非常容易了。

这个是封装的自动
cancel

fetch
函数,
myFetch.ts
文件代码如下:

import { getCurrentWatcher, onWatcherCleanup } from "vue";

export default async function myFetch(url: string, options: RequestInit) {
  const controller = new AbortController();
  if (getCurrentWatcher()) {
    onWatcherCleanup(() => {
      controller.abort();
    });
  }

  const res = await fetch(url, {
    ...options,
    signal: controller.signal,
  });

  let json;
  try {
    json = await res.json();
  } catch (error) {
    json = {
      code: 500,
      message: "JSON format error",
    };
  }
  return json;
}

由于
onWatcherCleanup
函数是从vue中import导入,那么我们就可以在自己封装的
myFetch
函数中导入和使用他。


onWatcherCleanup
函数的回调中我们执行了
controller.abort()
,前面已经讲过了当
watch
或者
watchEffect
的回调执行前或者组件卸载前就会执行里面的
onWatcherCleanup
注册的回调。我们这里的
myFetch
是在
watch
中调用的,当然也会触发里面的
onWatcherCleanup
注册的回调。


onWatcherCleanup
的回调中执行了
controller.abort()
,前面我们讲过了执行
controller.abort()
就会将正在请求的fetch函数给cancel取消掉。

就这么简单的就实现了前面的两个需求:

需求一:
如果
id
的值变化很快,但是服务端接口请求需要2秒才能完成,这时我们期望只有最后一次
id
的值改变触发的请求才需要完成,其他请求都cancel取消掉。

下面这个是变量id在短时间内多次修改的gif效果图:
click

从上面的gif图可以看到只有最后一个请求是完成了的,其他请求全部被cancel掉。

需求二:
如果在
myFetch
请求的过程中组件被销毁了,此时我们也期望能够将请求cancel取消掉。

下面这个是组件卸载时gif效果图:
hide

从上图中可以看到在卸载组件时组件正在从服务端请求数据,此时请求会自动cancel掉。

细心的小伙伴发现了在
myFetch
函数中,
onWatcherCleanup
函数外面套了一个
getCurrentWatcher
的判断,代码如下:

import { getCurrentWatcher, onWatcherCleanup } from "vue";

export default async function myFetch(url: string, options: RequestInit) {
  // ...省略
  if (getCurrentWatcher()) {
    onWatcherCleanup(() => {
      controller.abort();
    });
  }
  // ...省略
}

当watch或者watchEffect监听的值改变后
onWatcherCleanup
的回调就会触发,所以
onWatcherCleanup
的执行是由其所在的watch或者watchEffect触发的。

如果
onWatcherCleanup
不在watch或者watchEffect的回调中执行,那么当然
onWatcherCleanup
中的回调也永远不会执行。

可能有的小伙伴有疑问,你这里的
onWatcherCleanup
是在
myFetch
中执行的,也没在watch或者watchEffect的回调中执行吖?

答案是
myFetch
函数的执行是在watch中执行的,
myFetch
然后再去执行
onWatcherCleanup


getCurrentWatcher()
函数就会返回当前
正在执行回调
的watch或者watchEffect,如果当前
myFetch
不是在watch或者watchEffect的回调中执行的,那么
getCurrentWatcher()
函数的返回值就是空,所以这种情况就不需要去执行
onWatcherCleanup
函数了。

最后值得一提的是
onWatcherCleanup
不能在await后面执行,比如下面这样的代码:

import { getCurrentWatcher, onWatcherCleanup } from "vue";

export default async function myFetch(url: string, options: RequestInit) {
  const controller = new AbortController();
  const res = await fetch(url, {
    ...options,
    signal: controller.signal,
  });

  let json;
  try {
    json = await res.json();
  } catch (error) {
    json = {
      code: 500,
      message: "JSON format error",
    };
  }
  // ❌ 错误的写法
  if (getCurrentWatcher()) {
    onWatcherCleanup(() => {
      controller.abort();
    });
  }

  return json;
}

在上面的代码中我们将
onWatcherCleanup
调用放在了
await fetch()
的后面,这种写法
onWatcherCleanup
注册的
回调是不会执行的

为什么在
await
后面的
onWatcherCleanup
注册的回调永远不会执行呢?

答案是js的await相当于注册了一个回调函数去执行await后的代码,当await等待结束后再去执行这个回调函数,从而执行await后的代码。

await以及之前的代码确实是在watch回调中执行的,我们这里的
onWatcherCleanup
就是await后面的代码,await后面的代码是在一个新的回调中执行的,也就是watch“回调中”的“回调中”执行的。


onWatcherCleanup
执行时已经不知道当前正在执行的watch回调是谁了,所以
onWatcherCleanup
的回调也没注册上。当watch的变量修改时或者组件卸载时
onWatcherCleanup
注册的回调永远也不会执行。

总结


watch
或者
watchEffect
监听的变量修改时,以及组件卸载时,会去执行他们回调中使用
onWatcherCleanup
注册的回调函数。并且
onWatcherCleanup
是从vue中import导入的,使得我的可以在任意地方执行
onWatcherCleanup
函数。利用这两个特性我们就可以封装一个自动cancel的fetch函数。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

引言

近年来,随着软件开发行业的蓬勃发展,越来越多的编程语言和框架进入了市场,成为了不同类型软件开发项目的首选工具。然而,在中国的开发者社区中,.NET 开发人员的工资水平相比其他技术栈,如 Java、Python 和前端开发人员,往往偏低。这种现象引发了广泛的讨论和思考:为什么 .NET 作为一个强大的、广泛应用于企业级开发的框架,在中国的薪酬待遇普遍不如其他技术栈?本文将从多个角度深入分析.NET在中国工资偏低的原因,探讨技术选择、市场需求、企业文化等多个因素对.NET 开发者薪资水平的影响。

1. .NET 在中国的市场定位和历史背景

1.1 .NET 框架的历史发展

.NET 框架由微软在2002年首次发布,最初定位为一个跨语言的开发平台,旨在简化 Windows 应用程序的开发。凭借强大的 Visual Studio IDE、广泛的库支持和良好的文档,.NET 成为众多企业在内部系统开发中的首选。尤其是在 Windows 操作系统广泛使用的时代,.NET 被广泛用于企业应用、桌面软件和 Web 应用开发。

然而,随着时间的推移,微软的闭源策略限制了 .NET 的发展,特别是在开源浪潮席卷全球的背景下,.NET 框架显得有些陈旧和封闭。直到 2016 年,微软推出了 .NET Core,并开源了部分框架代码,试图重新夺回开发者的青睐。然而,在这段时间里,Java、Python 等开源语言已经占据了大量市场份额,特别是在中国,Java 成为了企业开发的首选。

1.2 .NET 在中国的应用领域

在中国,.NET 主要被应用于大型企业的内部系统、ERP 系统、OA 系统等领域。这些系统的开发需求相对固定,并且多数基于 Windows 服务器运行。这意味着,.NET 的市场需求主要集中在传统企业中,而非新兴的互联网企业或初创公司。

相比之下,Java、Python 和 JavaScript 等技术栈广泛应用于互联网应用、云计算、大数据、人工智能等领域,这些领域的需求量大且增长迅速。随着互联网行业的崛起,基于 Java 和其他开源技术栈的开发需求猛增,而 .NET 主要局限于传统行业,需求增长相对滞后。

2. 市场需求与技术栈选择

2.1 互联网行业对技术栈的偏好

互联网行业是中国近十年来发展最为迅猛的领域之一,众多的初创公司和科技巨头如阿里巴巴、腾讯、百度、美团等都选择了以 Java、Python、Node.js 等开源技术为主的技术栈。相比之下,.NET 的封闭性和 Windows 系统的依赖性让它在这一市场中显得不具备竞争力。

此外,互联网应用对快速迭代、敏捷开发的需求更高,开源技术栈提供了更为灵活的扩展性和更低的技术成本,这使得企业更倾向于选择 Java、Python、JavaScript 等技术栈进行开发。与之相对的,.NET 框架虽然功能强大,但其企业级的特性和较高的学习曲线限制了其在互联网行业中的应用广度。

2.2 人才供需失衡

.NET 在中国的市场需求主要集中在传统企业的内部系统开发中,这些企业的项目生命周期较长,技术更新缓慢,开发需求相对稳定。这意味着 .NET 开发人员的需求增长缓慢,市场上已经有一批稳定的开发人员在这个领域工作。

相比之下,互联网和新兴技术领域对 Java、Python、前端开发等技能的需求呈爆炸性增长,企业为了吸引和留住这些技术栈的开发人员,不得不提高薪资水平,导致这类开发人员的工资水涨船高。

3. .NET 技术的局限性

3.1 跨平台性较晚

在 .NET Core 推出之前,传统的 .NET 框架主要针对 Windows 平台开发,限制了其在 Linux 和 macOS 等操作系统上的应用。这与 Java 等技术形成了鲜明对比,后者在跨平台能力上具有天然的优势。

虽然 .NET Core 引入了跨平台支持,并逐渐得到了开发者的认可,但其市场渗透速度较慢,尤其是在中国,很多企业仍然停留在传统的 .NET 框架上。跨平台性较晚的发展,导致 .NET 没能在多平台应用领域获得更多市场份额,这也限制了其开发者的职业发展空间和薪资水平。

3.2 开发生态与社区活跃度

相较于 Java、Python 和 JavaScript 等技术栈,.NET 的开发者社区在中国的活跃度较低。这一方面是因为 .NET 曾经长期处于闭源状态,开发者习惯依赖微软的官方文档和工具,而不是通过社区贡献来推动技术的发展。另一方面,.NET 技术更新的频率相对较低,很多开发人员习惯了相对封闭的生态环境,导致其在技能更新和自我提升上缺乏动力。

社区的活跃度直接影响到技术的推广和发展。在中国,Java、Python 等技术拥有大量的开源项目、活跃的开发者论坛和线下技术交流会,这为开发者提供了丰富的学习资源和成长空间。而 .NET 的生态相对封闭,开发者的职业发展路径相对狭窄。

4. 企业文化与招聘需求

4.1 国有企业和传统行业的主导

在中国,.NET 的主要应用场景集中在国有企业、政府部门和一些传统行业。这些行业的项目通常是内部信息化系统,如 ERP、OA 等,项目周期长,技术更新缓慢。这些企业往往追求稳定性和可维护性,而不是快速的技术迭代,因此对开发人员的创新能力和技术广度要求较低。

此外,这类企业的薪酬体系相对保守,尤其是在与互联网企业相比时,薪资增长空间有限。这导致了在这些行业中工作的 .NET 开发者的工资水平长期保持在一个相对较低的水平。

4.2 互联网企业的招聘偏好

互联网企业对开发人员的技术创新能力、学习能力和跨平台开发能力要求较高。Java、Python 等开源技术的生态系统更适合满足这些需求,而 .NET 开发人员的技能集相对局限于企业内部系统开发。

同时,互联网行业的快速发展带来了大量的高薪岗位,而这些岗位往往要求开发者具备互联网技术背景、开源项目经验和跨平台开发能力。这也导致 .NET 开发人员在互联网企业中的需求较低,工资水平相应受到影响。

5. 技术演进与未来趋势

5.1 .NET Core 和 .NET 6+ 的机遇

随着 .NET Core 和之后的 .NET 5、.NET 6 +的推出,微软大大增强了 .NET 的跨平台能力,并将其全面开源。这为 .NET 开发者提供了更多的职业发展机会,特别是在云计算、容器化、微服务等新兴技术领域。

然而,由于 .NET 长期以来的封闭性和市场局限性,许多开发者和企业对这一变化并未迅速跟进。在中国市场,企业对 .NET Core 的接受程度仍然较低,很多传统企业仍然停留在 .NET Framework 之上,导致这部分开发者的技能没有得到有效提升。

5.2 云计算和新兴技术的挑战

随着云计算、大数据、人工智能等技术的快速发展,企业对技术栈的要求也在不断变化。虽然 .NET Core 提供了一些现代化的开发工具和特性,但 Java、Python 和其他技术栈在这些领域已经占据了主导地位,特别是在云计算平台如 AWS、阿里云等上,Java 和 Python 的支持更加完善。

此外,新兴技术领域对开源项目的依赖程度较高,开源技术栈更容易融入这些领域。而 .NET 的历史背景让许多企业对其在新兴技术中的应用持观望态度。这也限制了 .NET 开发者在新兴领域的工资增长空间。

6. 结论

.NET 在中国的工资水平相对较低,主要是由于其市场定位、技术局限性和行业需求的综合影响。尽管 .NET 具有强大的开发能力和稳定性,但其长期以来的封闭性以及在跨平台和开源浪潮中的迟缓反应,导致了其在快速发展的互联网行业中失去了竞争力。然而,随着 .NET Core 的推出和技术的现代化,.NET 开发人员面临着新的机遇和挑战。虽然传统行业的需求仍然存在,但未来的工资增长很可能依赖于开发人员能否快速适应技术趋势,并在新兴的开发领域中取得突破。以下是一些影响未来 .NET 开发者职业发展和薪资水平的关键因素。

7. .NET 开发者的未来机遇

7.1 跨平台与开源的推动

随着 .NET Core 以及现在的 .NET 6 和未来的 .NET 版本的全面开源和跨平台支持,微软正在积极融入现代开发生态系统。这意味着 .NET 开发者不仅可以在 Windows 环境下工作,还能够在 Linux、macOS 等操作系统中进行开发。这为 .NET 开发人员进入更多应用场景提供了机会,尤其是在微服务、容器化应用、云原生开发等领域。

中国的开发市场正在迅速转向开源和跨平台技术,企业也逐渐认识到跨平台开发的价值。这为 .NET 开发人员提供了重要的转型契机,特别是那些愿意不断学习并掌握新工具的开发者,将在未来的市场竞争中占据有利地位。通过掌握 Docker、Kubernetes、Azure 和 AWS 等云平台技术,.NET 开发者可以拓宽自己的技能领域,获得更高的职业增长空间。

7.2 云计算和微服务架构

云计算和微服务是未来 IT 发展的核心方向之一。虽然 .NET 在中国传统行业中占有重要地位,但其在云计算领域的应用仍然相对较少。然而,随着 Azure 和其他云平台的大力推广,.NET 已经具备了在云原生架构中的强大能力。尤其是通过与 Azure DevOps、Kubernetes 等工具的集成,.NET 开发人员可以在分布式系统和云原生架构中发挥更大作用。

此外,微软 Azure 在全球范围内的市场份额不断增加,越来越多的企业选择将其工作负载迁移到云端,而 .NET 作为微软自家产品的核心开发框架,在 Azure 平台上有天然的优势。对于那些熟悉云计算架构和服务的 .NET 开发人员来说,工资和职业前景都将得到显著提升。

7.3 企业级开发需求的长期存在

尽管互联网和新兴技术行业快速发展,但企业级软件开发的需求依然强劲。许多大型企业,尤其是国有企业和金融机构,仍然依赖于 .NET 框架来构建其核心系统。对于这些企业来说,稳定性、安全性和高性能是首要考虑因素,而 .NET 恰好在这些领域表现优异。

这些企业对人才的需求虽然不像互联网企业那样频繁快速,但其对经验丰富的 .NET 开发人员的长期需求不会消失。这意味着,在这些企业中工作,尽管工资增长可能不会像互联网行业那么迅速,但工作稳定性和长期的职业发展机会依然存在。

7.4 .NET 社区的成长

虽然 .NET 社区在中国的活跃度相较于 Java 和其他开源技术仍有一定差距,但随着微软加大对 .NET 开源项目的投入,以及全球范围内开发者社区的推动,.NET 社区正在逐渐壮大。越来越多的开发者开始贡献开源项目、分享技术经验,这种趋势在未来可能会促进更多 .NET 开发者加入开源生态系统,从而提升其市场价值。

8. 影响 .NET 开发者工资的其他因素

8.1 技术多样性与深度

.NET 开发者的工资水平不仅取决于对 .NET 技术栈的掌握程度,还与开发者的技术多样性和深度有关。一个仅熟悉传统 .NET 框架的开发者,面对未来的职业发展时可能会遇到瓶颈。而那些掌握了最新的 .NET Core、云计算技术、微服务架构和前端开发的开发者,其市场竞争力和薪资水平将远高于单一技术的开发者。

具备多种技术能力的开发人员在面对技术变革时更加灵活,可以适应不同的开发需求和项目类型。例如,能够同时熟悉 .NET 和前端开发技术(如 Angular 或 React)的开发人员,往往能够获得更高的薪酬,因为他们在项目中可以承担更多的职责。

8.2 区域差异

中国不同地区的经济发展水平和行业需求对 .NET 开发者的工资也有显著影响。在北京、上海、深圳等一线城市,尽管 .NET 的需求相对较少,但这些城市对高端开发人员的需求量依然较大,.NET 开发者的薪资水平可能高于二三线城市。

与此相对,二三线城市的企业更多集中于传统行业,这些行业对 .NET 技术的依赖较强,但其工资水平相对较低。虽然开发者的需求相对稳定,但企业对高薪技术人才的吸引力有限。因此,区域差异在很大程度上决定了 .NET 开发者的薪资水平。

8.3 经验和项目背景

经验丰富的 .NET 开发人员在特定行业中的积累,也会影响其薪资水平。例如,在金融、政府和医疗等对系统稳定性和安全性要求极高的行业,经验丰富的开发人员可能会获得更高的薪资待遇。这些行业中的项目往往涉及复杂的业务逻辑和严格的合规要求,熟悉行业规范的 .NET 开发者在这些领域非常受欢迎。

相较于初级开发人员,资深开发人员能够承担更多的责任,尤其是架构设计和技术领导方面的角色。具有多年项目管理经验和架构设计经验的开发人员,不仅技术水平高,且能够有效地领导团队完成复杂项目,这也是他们工资高于普通开发者的原因之一。

9. 总结与展望

.NET 开发人员在中国市场上工资相对较低的现象,可以归因于多个复杂因素,包括市场需求、技术选择、企业文化和历史发展背景。随着 .NET 技术的演进,特别是 .NET Core 和 .NET 6 的推出,跨平台能力和开源生态的增强为 .NET 开发者带来了新的机遇。然而,由于中国市场的互联网行业和新兴技术领域对 Java、Python 等开源技术的偏好,.NET 开发者的职业发展和薪资水平仍然面临一定挑战。

未来,.NET 开发者若能积极拥抱新技术,特别是云计算、微服务、前端开发等领域,将能够在市场上获得更高的竞争力。同时,随着 .NET 社区的成长和微软对云平台的推动,.NET 开发者在新兴技术领域的应用前景也将逐渐拓宽。

对于那些愿意持续学习并适应技术变革的 .NET 开发者来说,未来的职业前景依然充满机遇。虽然当前的工资水平相对较低,但通过技术深耕和跨领域的技能提升,开发者完全有可能在未来获得更高的薪资待遇和职业成就。在中国这样一个高速发展的技术市场中,灵活应变和持续创新将是所有开发者成功的关键。

补: Rest 风格请求处理的的内容补充(1)

Rest风格请求:注意事项和细节

  1. 客户端是PostMan 可以直接发送Put,delete等方式请求,可不设置Filter

  2. 如果哟啊SpringBoot支持页面表达的 Rest 功能,则需要注意如下细节:

  1. Rest 风格请求核心 Filter: HiddenHttpMethodFilter,表单请求会被 HiddenHttpMethodFilter拦截,获取到表单_method的值,再判断PUT/DELETE/PATCH(patch方法是新引入的,是对Put方法的补充,用来对已知资源进行局部更新:)
    https://segmentfault.com/q/1010000005685904
  2. 如果要SpringBoot 支持页面表单的Rest功能,需要在application.yml 启用 filter功能,否则无效。
  3. 修改application.yml (resources 类路径下) 启用 filter 功能。

在这里插入图片描述

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true # 开启页面表单的rest功能,启用了HiddenHttpMethodFilter,支持rest

Rest的核心过滤器:

  1. 当前的浏览器只支持 post/get请求,因此为了得到 put/delete的请求方式需要提供的 HiddenHttpMethodFilter过滤器进行转换

  2. HiddenHttpMethodFilter : 浏览器 form 表单只支持 get 和 post 请求,而delete,put 等method并不支持,
    spring添加了一个过滤器,可以将这些请求转换为标准的 http 方使得支持get,post,put和delete请求

  3. HiddenHttpMethodFilter 能对 post 请求方式进行转换,因此我们需要特别的注意这一点

  4. 这个过滤器需要在 web.xml 中配置

Spring Boot 开启视图解析器的 yaml 语法

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true # 开启页面表单的rest功能,启用了HiddenHttpMethodFilter,支持rest
    view: # 配置视图解析器
      prefix: /rainbowsea/** # 这里是需要注意,如果你配置了 static-path-pattern: /rainbowsea/** 需要保持一致
#      prefix: /rainbowsea/ 都行 # 这里是需要注意,如果你配置了 static-path-pattern: /rainbowsea/** 需要保持一致
      suffix: .html
    static-path-pattern: /rainbowsea/**

我们这里思考一个问题:
为什么这里return "hello",返回的是不是字符串,而是转发到对应的资源文件。

在这里插入图片描述

package com.rainbowsea.springboot.controller;


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HiController {


    @RequestMapping("/hello")
    public String hi(){
        return "hi:):)";
    }


    @RequestMapping("/go")
    public String go(){
        return "hello";
        /*

        return 是先看视图解析器当中是否有 hello.html 页面,没有就在找 controller 控制
是否有处理该请求的,如果两者都没有则报 404错误
         */
    }

}

在这里插入图片描述

注意:我是配置了视图解析器的。
在这里插入图片描述

启动 Spring Boot ,打开浏览器输入:
http://localhost:8080/go

在这里插入图片描述

在这里插入图片描述

当 hello.html 静态资源存在时,并没有走 controller

我们将静态文件资源
hello.html
移除,再次访问:
http://localhost:8080/go

问题:

我们将静态文件资源
hello.html
移除,再次访问:
http://localhost:8080/go

在这里插入图片描述

在这里插入图片描述

最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

多边形
是常见的几何结构,它的形状看似千变万化,其实都可以由几种常用的多边形组合而成。

本篇介绍
manim
中提供的几个绘制
常用多边形
的模块。

  1. Triangle
    :等边三角形
  2. Square
    :正方形
  3. Rectangle
    :长方形
  4. RoundedRectangle
    :圆角的长方形
  5. Star
    :没有相交线的正多边形,图形类似带尖角的星形

1. 主要参数

这几个模块中,
Triangle
最简单,它没有自己特有的参数。

Square
有一个参数:

参数名称 类型 说明
side_length float 正方形边的长度

Rectangle
略微复杂一些,它可以平均分块形成表格。

参数名称 类型 说明
height float 长方形的高度
width float 长方形的宽度
grid_xstep float 划分长方体后,每列的宽度
grid_ystep float 划分长方体后,每行的高度

RoundedRectangle
继承自
Rectangle
,可以使用
Rectangle
的所有参数,

此外,它还一个自己特有的参数。

参数名称 类型 说明
corner_radius float list[float]

RoundedRectangle
四个角的曲率可以统一设置,也可以设置成不同的曲率。

Star
模块之所以是这个名称,是因为它绘制出的图形像小星星。

参数名称 类型 说明
n int 星形图形有多少个尖角
outer_radius float 图形的外接圆半径
inner_radius float 图形的内切圆半径
density int 图形尖角的密度,inner_radius为设置时才有效
start_angle float 顶点开始的角度

如果对这些属性的含义看不明白也不要紧,后面结合示例展示星形图形在不同参数下的区别,

就能看的更明白一些了。

2. 使用示例

2.1. 等边三角形和正方形

等边三角形
Triangle
算是最简单的多边形了,它没有参数,

但是可以通过
scale

rotate
等方法了改变它的大小和角度。

Triangle()

# 放大1.5倍
Triangle().scale(1.5)

# 旋转180度
Triangle().rotate(PI)

正方形
Square
也简单,它只有一个参数,设置正方形的边长。

Square(side_length=0.5)
Square(side_length=1)
Square(side_length=2)

上面代码的显示效果如下:

2.2. 长方形

长方形
Rectangle
除了可以设置宽度
width
和高度
height
,还可以对其进行分块。

所谓分块,就是通过
grid_xstep

grid_ystep
参数讲长方形分割为一个个更小的矩形。

每个小矩形的宽度为
width / grid_xstep
,高度为
height / grid_ystep

Rectangle(width=2, height=1)
Rectangle(width=1, height=3)

# 分割为2行3列的矩形
Rectangle(
    width=3,
    height=2,
    grid_xstep=1,
    grid_ystep=1,
)

2.3. 圆角长方形

圆角长方形
RoundedRectangle

长方形
Rectangle
的区别在于,它可以设置4个角的曲率。

Rectangle
具有的参数,
RoundedRectangle
也可以使用,包括分块的参数。

# 4个角的曲率相同
RoundedRectangle(
    corner_radius=0.4,
)
# 对角曲率相同
RoundedRectangle(
    corner_radius=[0.2, 0.6],
)

# 4个角曲率都不同
RoundedRectangle(
    corner_radius=[0.1, 0.6, 0.3, 0.9],
)

2.4. 星形

星形多边形
Star
是一种特殊的凹多边形,因其独特的形状和对称性,常被用作装饰图案和设计元素。

Star
模块可以通过参数尖角的个数以及尖角的密度。

Star(n=5)

# density越大,尖角看上去越密集
Star(n=9, density=2)
Star(n=9, density=4)

3. 附件

文中完整的代码放在网盘中了(
polygon01.py
),

下载地址:
完整代码
(访问密码: 6872)