写点什么

C# 8 的 Ranges 和递归模式

2018 年 8 月 07 日

关键要点

  • C# 8 新增了 Ranges 和递归模式。
  • 可以使用 Ranges 来定义数据序列,可用于替代 Enumberable.Range()。
  • 递归模式为 C#带来了类似 F#的结构。
  • 递归模式是一个非常棒的功能,为我们提供了一种灵活的方式,基于一系列条件来测试数据,并根据满足的条件执行进一步的计算。
  • Ranges 可用于生成集合或列表形式的数字序列。

2015 年 1 月 21 日是 C#历史上最重要的日子之一。在这一天,C#专家 Anders Hejlsberg 和 Mads Torgersen 等人聚在一起畅谈 C#的未来,并思考了这门语言应该往哪个方向发展。

2015 年 1 月 21 日的 C#会议纪要。

这次会议的第一个结果是 C# 7。第七个版本增加了一些新特性,并将重点放在数据消费、代码简化和性能上。针对 C# 8 的新提议并未改变对特性的关注,但在最终版本中可能会有所改变。



图 1. C# 7 和 8 的关注点

在本文中,我将讨论为 C# 8 提议的两个新特性。第一个是 Ranges,第二个是递归模式,它们都属于代码简化类别。我将通过很多示例详细地解释它们,我将向你展示这些特性如何帮助你写出更好的代码。

Ranges 可用于定义数据序列。它是 Enumerable.Range() 的替代品,只是它定义的是起点和终点,而不是起点和计数,它可以帮助你写出可读性更高的代码。

示例

复制代码
foreach(var item in 1..100)
{
Console.WriteLine(item);
}

递归模式匹配是一个非常强大的功能,主要与递归一起使用,可用它写出更加优雅的代码。 RecursivePatterns 包含多个子模式,例如位置模式(Positional Pattern,var isBassam = user is Employee(“Bassam”,_))、属性模式(Property Patterns,p is Employee {Name is “Mais”})、变量模式(Var Pattern)、丢弃模式(Discard Pattern,'_'),等等。

示例

带元组的递归模式(下面的例子也称为元组模式)

复制代码
var employee = (Name: "Thomas Albrecht", Age: 43);
switch (employee)
{
case (_, 43) employeeTmp when(employeeTmp.Name == "Thomas Albrecht "):
{
Console.WriteLine($ "Hi {employeeTmp.Name} you are now 43!");
}
break;
// 如果 employee 包含了其他信息,那么就执行下面的代码。
case _:
Console.WriteLine("any other person!");
break;
}

case (_,43) 可以解释如下:首先,“_”表示忽略 Name 属性,但 Age 必须为 43。如果 employee 元组包含 (任何字符串,43),则将执行 case 块。

尝试在这里运行上面的代码。



图 2. 递归模式的基本示例

我们过去曾在多篇文章中讨论过这个主题,但这是我们第一次深入研究模式匹配。

Ranges

这个特性是关于提供两个新的操作符(索引操作符“^”和范围操作符“..”),可以用它们来构造 System.Index 和 System.Range 对象,并使用它们在运行时对集合进行索引或切片。新的操作符其实是语法糖,让你的代码更加简洁。操作符索引 ^ 的代码使用 System.Index 实现,在范围操作符“..”使用 System.Range 实现。

System.Index

从结尾处对集合进行索引的绝佳方式。

示例

var lastItem = array[^1]; 与 var lastItem = array[collection.Count-1]; 是等效的。

System.Range

这是一种访问集合的“范围”或“切片”的方式。这样可以避免使用 LINQ,并让代码更加紧凑,可读性更高。你可以将它与 F#中的 Ranges 进行比较。

新的风格

旧的风格

var thirdItem = array [2]; 

// 后台的代码: array [2]

var thirdItem = array [2]; 

var lastItem = array [^1];

// 后台的代码: [^1] = new Index(1, true);

var lastItem = array [array.Count -1];

var lastItem = array.Last; // LINQ

var subCollection = array[2..^5]; // 输出: 2, 3, 4, 5

// 后台的代码: Range.Create(2, new Index(5, true)); 我们使用了两种操作符 Range 和 Index。Range 对应操作符..Index 对应操作符 ^。意思是从头开始跳到索引 2 的位置,^5 表示忽略从头开始的 5 个元素。

var subCollection = array.ToList().GetRange(2, 4);

使用 LINQ 就是:

var subCollection = array.Skip(2).Take(4);

示例

考虑下面的数组:

var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 

 

 Value 

 1 

 2 

 3 

 4 

 5 

 6 

 7 

 8 

 9 

 10 

我们可以使用以下索引访问数组的值:

 Index 

 1 

 2 

 3 

 4 

 5 

 6 

 7 

 8 

 9 

 10 

现在,我们从这个数组中剪切出一个切片视图,如下所示:

var slice= array[2..5];

我们可以使用以下索引访问切片的值:

注意:起始索引是被包含在切片中的,而结束索引是不包含在切片中的。

复制代码
var slice1 = array [4..^2]; // Range.Create(4, new Index(2, true))

slice1 的类型为 Span<int>。[4..^2] 从开始跳到索引 4,并从结尾跳过 2 个位置。

复制代码
Output: 4, 5, 6, 7, 8
var slice2 = array [..^3]; // Range.ToEnd(new Index(3, true))
Output: 0, 1, 2, 3, 4, 5, 6, 7
var slice3 = array [2..]; // Range.FromStart(2)
Output: 2, 3, 4, 5, 6, 7, 8,9, 10
var slice4 = array[..]; // array[Range.All]
Output: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

可以在这里运行代码示例。

有边界 Ranges

在有边界 Ranges 中,下限(起始索引)和上限(结束索引)是已知的或预定义的。

复制代码
array[start..end] // 获取从 start-1end-1 的项
array[start..end:step] // 按照指定步长获取从 start-1end-1 的项

上面的 Range 语法(后面跟上步长)源自 Python。Python 支持这样的语法(lower:upper:step),其中:step 是可选的,默认为 1,但社区中有一些人希望使用 F#的语法(lower..step..upper)。

你可以在此处跟进讨论: Range 操作符

F#中的 Range 语法。

array { 5 .. 2 .. 20 } // 这里 2 = step [start .. step .. end]

输出:

5 7 9 11 13 15 17 19

有界 Range 示例

复制代码
var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var subarray = array[3..5]; // 选择的项为: 3, 4

上面的代码等同于 array.ToList().GetRange(3,2);。如果将 array.ToList().GetRange(3,2); 和 array[3..5] 进行对比,可以看出新的风格更清晰,更具人性化。

有一个功能请求是在“if”语句中使用 Range,或者使用如下所述的模式匹配:

使用“in”操作符

复制代码
var anyChar = 'b';
if (anyChar in 'a'..'z')
{
Console.WriteLine($"The letter {anyChar} in range!");
}
Output: The letter b in range!

Range 模式是新出现的模式匹配,可用于生成简单范围检查。在使用 Range 模式时,可在 switch 语句中使用 Range 操作符“..”。

复制代码
switch (anyChar)
{
case in 'a'..'z' => Console.WriteLine($“The letter {anyChar} in range!”),
case in '!'..'+' => Console.WriteLine($“Something else!”),
}
Output: The letter b in range!

值得一提的是,并非所有人都喜欢在 Ranges 中使用“in”操作符。社区中有人使用“in”,有人使用“is”,你可以在这里跟进整个讨论: C# Range 的问题

无边界 Ranges

当省略下限时,默认为零,而当上限被省略时,默认为集合的长度。

示例

复制代码
array[start..] // 获取从 start-1 开始的所有项
array[..end] // 获取从头开始到 end-1 的项
array[..] // 获取真个数组

正边界

复制代码
var fiveToEnd = 5..; // 等同于 Range.From(5),也即缺失上界
var startToTen = ..1; // 等同于 Range.ToEnd(1),也即缺失下届,结果为: 0, 1
var everything = ..; // 等同于 Range.All,也即缺失上届和下届,结果为: 0..Int.Max
var everything = [5..11]; // 等同于 Range.Create(5, 11)
var collection = new [] { 'a', 'b', 'c' };
collection[2..]; // 输出: c
collection[..2]; // 输出: a, b
collection[..]; // 输出: a, b, c

负边界

你可以使用负边界。它们表示相对于集合的长度,1 表示最后一个元素,2 表示倒数第二个元素,依此类推。

示例

复制代码
var collection = new [] { 'a', 'b', 'c' };
collection[-2..2]; // 结果: b, c
collection[-1..]; // 结果: c
collection[-3..-1]; // 结果: a, b

注意:目前,负面界限无法测试,如下所示:



图 3. 使用负索引导致的参数异常

Ranges 与字符串

可以使用索引来创建子字符串:

示例

复制代码
var helloWorldStr = "Hello, World!";
var hello = helloWorldStr[..5];
Console.WriteLine(hello); // Output: Hello
var world = helloWorldStr[7..];
Console.WriteLine(world); // Output: World

或者可以这样写:

复制代码
var world = helloWorldStr[^6..]; // 获取最后 6 个字符
Console.WriteLine(world); // Output: World

Ranges 的 ForEach 循环

示例

使用 Ranges 来实现 IEnumerable<int>,可以对数据序列进行迭代。

复制代码
foreach (var i in 0..10)
{
Console.WriteLine(“number {i}”);
}

递归模式

模式匹配是一种功能强大的结构,出现在很多函数式编程语言中,如 F#。此外,模式匹配提供了解构匹配对象的能力,让你可以访问其数据结构的各个部分。C#为此提供了一组丰富的模式。

模式匹配最初计划出现在 C# 7 中,但后来.Net 团队发现他们需要更多时间来完成这个特性。因此,他们将这个任务分为两个部分。基本模式匹配已经在 C# 7 可用,而高级匹配模式则放在了 C# 8 中。我们已经在 C# 7 中看到了常量模式(Const Pattern)、类型模式(Type Pattern)、变量模式(Var Pattern)和丢弃模式(Discard Pattern)。在 C# 8 中,我们将看到更多的模式,如递归模式,它由多个子模式组成,如位置模式和属性模式。

要理解递归模式,需要很多示例代码。我已经定义了两个类。下面定义的 Employee 和 Company,我将用它们来解释递归模式。

复制代码
public class Employee
{
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
public Company Company
{
get;
set;
}
public void Deconstruct(out string name, out int age, out Company company)
{
name = Name;
age = Age;
company = Company;
}
}
public class Company
{
public string Name
{
get;
set;
}
public string Website
{
get;
set;
}
public string HeadOfficeAddress
{
get;
set;
}
public void Deconstruct(out string name, out string website, out string headOfficeAddress)
{
name = Name;
website = Website;
headOfficeAddress = HeadOfficeAddress;
}
}

位置模式

位置模式对匹配的类型进行分解,并基于返回的值执行进一步的模式匹配。这个模式的最终值为 true 或 false,决定了是否要执行后续的代码块。

复制代码
if (employee is Employee(_, _, ("Stratec", _, _)) employeeTmp)
{
Console.WriteLine($ "The employee: {employeeTmp.Name}!");
}
Output
The employee: Bassam Alugili

在这个例子中,我递归地使用了模式匹配。第一部分是位置模式 employee is Employee(…),第二部分是括号内的子模式 (_,_, (“Stratec”,_,_))。

if 语句之后的代码块只在位置模式(employee 对象必须是 Employee 类型)中的条件及其子模式 (_,_,(“Stratec”,_,_))(即 company 名称必须是“Stratec”)都满足时才会执行,其余部分被丢弃。

属性模式

属性模式很直接了当,你可以访问类型字段和属性,并对它们应用进一步的模式匹配。

复制代码
if (bassam is Employee {Name: "Bassam Alugili", Age: 42})
{
Console.WriteLine($ "The employee: {bassam.Name} , Age {bassam.Age}");
}

C# 6 风格:

复制代码
if (firstEmployee.GetType() == typeof(Employee))
{
var employee = (Employee) firstEmployee;
if (employee.Name == "Bassam Alugili" && employee.Age == 42)
{
Console.WriteLine($ "The employee: {employee.Name} , Age {employee.Age}");
}
}
// 或者我们可以这样做:
var employee = firstEmployee as Employee;
if (employee != null)
{
if (employee.Name == "Bassam Alugili" && employee.Age == 42)
{
Console.WriteLine($ "The employee: {employee.Name} , Age {employee.Age}");
}
}

将模式匹配代码与 C# 6 进行比较,可以看出 C# 8 代码更加明晰。新的风格移除了冗余代码和类型转换以及丑陋的操作符,如“typeof”或“as”。

递归模式

递归模式只不过是上述模式的组合。类型将被分解为子部分,让子部分与子模式匹配。实际上,递归模式通过使用 Deconstruct() 方法来解构类型,并在必要时基于解构值进行进一步的模式匹配。如果你的类型没有 Deconstruct() 方法或者不是元组,那么就需要自己编写这个方法。

如果从上面的 Company 类中删除 Deconstruct 方法,则会出现以下错误:

error CS8129: No suitable Deconstruct instance or extension method was found for type ‘Company’, with 0 out parameters and a void return type。

接下来让我们来看看位置模式和属性模式。

示例

我创建了两个 Employee 对象和两个 Company 对象,并分别进行了映射。

复制代码
var stratec = new Company
{
Name = "Stratec",
Website = "wwww.stratec.com",
HeadOfficeAddress = "Birkenfeld",
};
var firstEmployee = new Employee
{
Name = "Bassam Alugili",
Age = 42,
Company = stratec
};
var microsoft = new Company
{
Name = "Microsoft",
Website = "www.microsoft.com",
HeadOfficeAddress = "Redmond, Washington",
};
var secondEmployee = new Employee
{
Name = "Satya Nadella",
Age = 52,
Company = microsoft
};
DumpEmployee(firstEmployee);
DumpEmployee(secondEmployee);
public static void DumpEmployee(Employee employee)
{
switch (employee) {
case Employee(_, _, _) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! ");
}
break;
default:
Console.WriteLine("Other company!");
break;
}
}
Output
The employee: Bassam Alugili
The employee: Satya Nadella

在上面的示例中,case 将匹配包含数据的 Employee 对象,它是解构模式和丢弃模式的组合。现在我们将更进一步,只需要过滤 Stratec 的 employee。

使用模式匹配可以有多种方法。我们将使用一些不同的方式替换或重写以下的代码。

复制代码
case Employee(_, _, _) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! ");
}
break;

第一种方法,在 switch 语句中使用递归模式匹配(解构模式),如下所示。

用以下代码替换上面的代码。

复制代码
case Employee(_, _, ("Stratec", _, _)) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! ");
}
break;

输出:

复制代码
The employee: Bassam Alugili!
Other company!

第二种方法是使用警卫条件(Constraints)。

复制代码
case Employee(_, _, (_, _, _)) employeeTmp when employeeTmp.Company.Name == "Stratec":
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! ");
}
break;

同样,我们可以用不同的方式重写 case 表达式:

复制代码
case Employee(_, _,_) employeeTmp when employeeTmp.Company.Name == "Stratec":
case Employee employeeTmp when employeeTmp.Company.Name == "Stratec":

我们还可以将解构模式与变量模式结合起来,如下所示:

复制代码
case Employee(_, _,var (_,companyNameTmp,_)) employeeTmp when companyNameTmp == "Stratec":

另一种通过递归属性模式来过滤数据的方法,如下所示:

复制代码
case Employee {Company:Company{Name:"Stratec"}} employeeTmp:
Output for the above examples:
The employee: Bassam Alugili!
Other company!

在将 switch 语句与模式匹配一​​起使用时,需要注意一个重要的事项:

新的 switch 表达式的结构如下所示:

复制代码
 switch (value)
{
     case pattern guard => Code block to be executed
     ...
     case _ => default
}

回到我们的示例,看看以下的递归模式匹配示例:

复制代码
switch (employee)
{
case Employee {Name: "Bassam Alugili", Company: Company(_, _, _)} employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! 1");
}
break;
case Employee(_, _, ("Stratec", _, _)) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! 2");
}
break;
case Employee(_, _, Company(_, _, _)) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! 3");
}
break;
case Employee(_, _, _) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! 4");
}
break;
default:
Console.WriteLine("Other company!");
break;
}

上面的 switch 可以正常运行。如果我们将其中一个 case 向上或向下移动,比如将 case Employee(_,_,_) employeeTmp: 移动到开头,如下所示:

复制代码
switch (employee)
{
case Employee(_,_,_) employeeTmp:
{
Console.WriteLine($ "The employee: {employeeTmp.Name}! 4");
}
...
}

然后我们会得到以下错误:

  1. error CS8120: The switch case has already been handled by a previous case.
  2. error CS8120: The switch case has already been handled by a previous case.
  3. error CS8120: The switch case has already been handled by a previous case

图 4. 在 SharpLab 中移动 case 后出现的错误

编译器知道有些 case 是无法触及的(也就是死代码),并通过错误告诉你,你的代码写错了。

模式匹配与集合

示例

复制代码
switch (intCollection)
{
case [1, 2, var x ] =>
{
// 当 intCollection 中的头两个元素是 1 和 2 时,这个代码块会被执行,并且第 3 个元素会被复制给变量 x。
Console.WriteLine( $ "it's 1, 2, {x}", );
}
case [1,..20] =>
{
// 如果 intColleciton 以 1 为开头并以 20 结束,这个代码块会被执行。
);
case _ =>
{
// 如果上述两个 case 不匹配,这执行这个代码块。
}
}
if (intCollection is [.., 99, 100])
{
// 如果集合中的最后元素为 99 和 100,那么就执行这个代码块。
}
if (intCollection is [1, 2, ..])
{
// 如果集合中开始元素为 1 和 2,就执行这个代码块。
}
if (intCollection is [1, .., 100])
{
// 当集合中第一个元素是 1 并且最后一个元素是 100 时就执行这个代码块。
}

递归模式(C# 8)代码测试

  1. 复制以下代码示例
  2. 在 Web 浏览器中打开 https://sharplab.io
  3. 粘贴代码并选择“C# 8.0:RecusivePatterns(14 May 2018)”,然后选择“Run”,如图 5 所示。

或者,你可以使用我准备好的链接

代码:

复制代码
using System;
namespace RecursivePatternsDemo
{
class Program
{
static void Main(string[] args)
{
var stratec = new Company
{
Name = "Stratec",
Website = "wwww.stratec.com",
HeadOfficeAddress = "Birkenfeld",
};
var firstEmployee = new Employee
{
Name = "Bassam Alugili",
Age = 42,
Company = stratec
};
var microsoft = new Company
{
Name = "Microsoft",
Website = "www.microsoft.com",
HeadOfficeAddress = "Redmond, Washington",
};
var secondEmployee = new Employee
{
Name = "Satya Nadella",
Age = 52,
Company = microsoft
};
DumpEmployee(firstEmployee);
DumpEmployee(secondEmployee);
}
public static void DumpEmployee(Employee employee)
{
switch (employee)
{
case Employee {Name: "Bassam Alugili", Company: Company(_, _, _)} employeeTmp:
{
Console.WriteLine($"The employee: {employeeTmp.Name}! 1");
}
break;
case Employee(_, _, ("Stratec", _, _)) employeeTmp:
{
Console.WriteLine($"The employee: {employeeTmp.Name}! 2");
}
break;
case Employee(_, _, Company(_, _, _)) employeeTmp:
{
Console.WriteLine($"The employee: {employeeTmp.Name}! 3");
}
break;
default:
Console.WriteLine("Other company!");
break;
}
}
}
}
public class Company
{
public string Name
{
get;
set;
}
public string Website
{
get;
set;
}
public string HeadOfficeAddress
{
get;
set;
}
public void Deconstruct(out string name, out string website, out string headOfficeAddress)
{
name = Name;
website = Website;
headOfficeAddress = HeadOfficeAddress;
}
}
public class Employee
{
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
public Company Company
{
get;
set;
}
public void Deconstruct(out string name, out int age, out Company company)
{
name = Name;
age = Age;
company = Company;
}
}

图 5. SharpLab 设置

总结

在以集合或列表的形式生成数字序列时,Ranges 是非常有用的。将 Ranges 与每个循环或模式匹配等组合在一起,让 C#语法变得更加简洁易读。

递归模式是模式匹配的核心。模式匹配将运行时数据与任意数据结构进行比较,并将其分解为组成部分,或以不同的方式从数据中提取子数据,编译器将为你检查代码的逻辑。

递归模式是一个非常棒的功能,可以灵活地基于一系列条件对数据进行测试,并根据满足的条件执行进一步的计算。

关于作者

Bassam Alugili 是 STRATEC AG 的高级软件专家和数据库专家。STRATEC 是全自动分析仪系统、实验室数据管理软件和智能耗材的全球领先合作伙伴。

查看英文原文 C# 8 Ranges and Recursive Patterns

2018 年 8 月 07 日 18:289755
用户头像

发布了 731 篇内容, 共 368.6 次阅读, 收获喜欢 1860 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容

28天总结

张老蔫

28天写作

源中瑞智慧社区解决方案---助推平安小区建设

135深圳3055源中瑞8032

以终为始:28天打卡输出复盘

熊斌

个人成长 写作平台 28天写作

太傻了!下次二面再回答不好“秒杀系统“设计原理,我就捶死自己

Crud的程序员

Java 程序员 架构

LeetCode题解:529. 扫雷游戏,DFS,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

区块链知识产权保护解决方案,区块链存证解决方案

135深圳3055源中瑞8032

今年我读了四个开源项目的源码,来分享下心得

yes的练级攻略

源码 面试 后端

阿里大佬手码的SpringCloud+Alibaba笔记开源了,堪称保姆式教学

Crud的程序员

spring 程序员 架构

🌏 超详细 DNS 协议解析

飞天小牛肉

Java 程序员 面试 计算机网络 2月春节不断更

2021全新iOS学习方向

ios 逆向

图扑物联助力打造现代化智能港口

一只数据鲸鱼

物联网 工业4.0 组态软件 智慧港口

一周信创舆情观察(1.25~1.31)

统小信uos

面试加分项!我在美团Android研发岗工作的那5年,系列篇

欢喜学安卓

android 程序员 面试 移动开发

【春节不远行,云上过大年】来华为云社区,接任务领新年红包啦···

华为云开发者社区

华为云

腾讯云TcaplusDB获新加坡MTCS最高等级安全认证

TcaplusDB

数据库 nosql 数据安全 数据管理 tencentdb

源码阅读范例!终于有人把JDK源码精髓收录成册,全网开源了

程序员小毕

Java 源码 程序员 jdk 面试

淘宝的商品中心和类目体系是怎么设计的

邴越

阿里 模型 电商 架构· 业务

16张图带你吃透Redis架构演进

Kaito

redis 架构 高性能

原来这就是大厂的MySQL主从复制、读写分离及高可用方案!

云流

MySQL 数据库 架构

您的《操作系统线程模型总结》请查收。

后台技术汇

28天写作 2月春节不断更

GrowingIO SaaS 产品 CI/CD 实践 (一)

GrowingIO技术专栏

ci SaaS CD

面试加分项!零基础如何成为高级Android开发,先收藏了

欢喜学安卓

android 程序员 面试 移动开发

一寸宕机一寸血,十万容器十万兵|Win10/Mac系统下基于Kubernetes(k8s)搭建Gunicorn+Flask高可用Web集群

刘悦的技术博客

flask k8s kubectl Docker Desktop gunicorn

开放下载!解锁 Serverless 从入门到实战大“橙”就

阿里巴巴云原生

云计算 Linux Serverless 开发者 云原生

互助系统软件开发,互助app开发

luluhulian

《iOS面试简历技巧解析》

ios 面试

徒手撸一个Spring Boot中的starter

田维常

springboot

OpenYurt:延伸原生 Kubernetes 到边缘场景下的落地实践

阿里巴巴云原生

人工智能 容器 运维 云原生 k8s

基于SpringBoot的微服务架构与K8S容器部署实践

云流

Java 架构 微服务

PM必备自我管理工具整理

做人没有梦想和咸鱼有什么区别

项目管理 PM

“新内容 新交互” 阿里云全球视频云创新挑战赛正式开启!

阿里云视频云

阿里云 音视频 应用 音视频算法

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

C# 8的Ranges和递归模式-InfoQ