.NET Core 3.x.NET Standard 2.1支持C# 8.0

一、Readonly 成员

可将 readonly 修饰符应用于结构的成员,来限制成员为不可修改状态。这比在C# 7.2中将 readonly 修饰符仅可应用于 struct 声明更精细。

public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Distance => Math.Sqrt(X * X + Y * Y);
public override string ToString() =>
$"({X}, {Y}) is {Distance} from the origin";
}

与大多数结构一样,ToString() 方法不会修改状态。 可以通过将 readonly 修饰符添加到 ToString() 的声明来对此进行限制:

public readonly override string ToString() =>// 编译器警告,因为 ToString 访问未标记为 readonly 的 Distance 属性
$"({X}, {Y}) is {Distance} from the origin";
// Distance 属性不会更改状态,因此可以通过将 readonly 修饰符添加到声明来修复此警告
public readonly double Distance => Math.Sqrt(X * X + Y * Y);

注意:readonly 修饰符对于只读属性是必需的。

编译器会假设 get 访问器可以修改状态;必须显式声明 readonly

自动实现的属性是一个例外;编译器会将所有自动实现的 Getter 视为 readonly,因此,此处无需向 X 和 Y 属性添加 readonly 修饰符。

通过此功能,可以指定设计意图,使编译器可以强制执行该意图,并基于该意图进行优化。

二、默认接口方法

.NET Core 3.0 上的 C# 8.0 开始,可以在声明接口成员时定义实现。 最常见的方案是,可以将成员添加到已经由无数客户端发布并使用的接口。示例:

// 先声明两个接口
// 客户接口
public interface ICustomer
{
IEnumerable<IOrder> PreviousOrders { get; }
DateTime DateJoined { get; }
DateTime? LastOrder { get; }
string Name { get; }
IDictionary<DateTime, string> Reminders { get; } // 在客户接口中加入新的方法实现
public decimal ComputeLoyaltyDiscount()
{
DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
{
return 0.10m;
}
return 0;
}
}
// 订单接口
public interface IOrder
{
DateTime Purchased { get; }
decimal Cost { get; }
} // 测试代码
// SampleCustomer:接口 ICustomer 的实现,可不实现方法 ComputeLoyaltyDiscount
// SampleOrder:接口 IOrder 的实现
SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))
{
Reminders =
{
{ new DateTime(2010, 08, 12), "childs's birthday" },
{ new DateTime(1012, 11, 15), "anniversary" }
}
}; SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);
c.AddOrder(o);//添加订单
o = new SampleOrder(new DateTime(2103, 7, 4), 25m);
c.AddOrder(o); // 验证新增的接口方法
ICustomer theCustomer = c; // 从 SampleCustomer 到 ICustomer 的强制转换
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");
// 若要调用在接口中声明和实现的任何方法,该变量的类型必须是接口类型,即:theCustomer

三、模式匹配的增强功能

C# 8.0扩展了C# 7.0中的词汇表(isswitch),这样就可以在代码中的更多位置使用更多模式表达式。

3.1 switch 表达式

区别与 switch 语句:

  变量位于 switch 关键字之前;

  将 case 和 : 元素替换为 =>,更简洁、直观;

  将 default 事例替换为 _ 弃元;

  实际语句是表达式,比语句更加简洁。

public static RGBColor FromRainbow(Rainbow colorBand) =>
colorBand switch
{
Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
_ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
};

3.2 属性模式

借助属性模式,可以匹配所检查的对象的属性。

如下电子商务网站的示例,该网站必须根据买家地址(Address 对象的 State 属性)计算销售税。

// Address:地址对象;salePrice:售价
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
location switch
{
{ State: "WA" } => salePrice * 0.06M,
{ State: "MN" } => salePrice * 0.075M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};

此写法,使得整个语法更为简洁。

3.3 元组模式

日常开发中,存在算法依赖于多个输入。使用元组模式,可根据 表示为元组 的多个值进行切换。

// 游戏“rock, paper, scissors(石头剪刀布)”的切换表达式
public static string RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
("paper", "rock") => "paper covers rock. Paper wins.",
("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors is broken by rock. Rock wins.",
("scissors", "paper") => "scissors cuts paper. Scissors wins.",
(_, _) => "tie" // 此处弃元 表示平局(石头剪刀布游戏)的三种组合或其他文本输入
};

3.4 位置模式

某些类型包含 Deconstruct 方法,该方法将其属性解构为离散变量。 如果可以访问 Deconstruct 方法,就可以使用位置模式检查对象的属性并将这些属性用于模式。

// 位于象限中的 点对象
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
public enum Quadrant// 象限
{
Unknown, Origin, One, Two, Three, Four, OnBorder
}
// 下面的方法使用位置模式来提取 x 和 y 的值。 然后,它使用 when 子句来确定该点的 Quadrant
static Quadrant GetQuadrant(Point point) => point switch
{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,// 当 x 或 y 为 0(但不是两者同时为 0)时,前一个开关中的弃元模式匹配
_ => Quadrant.Unknown
};

如果没有在 switch 表达式中涵盖所有可能的情况,编译器将生成一个警告。

四、using 声明

using 声明是前面带 using 关键字的变量声明。它指示编译器声明的变量应在封闭范围的末尾进行处理。

static int WriteLinesToFile(IEnumerable<string> lines)
{
using var file = new System.IO.StreamWriter("WriteLines2.txt");
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
file.WriteLine(line);
else
skippedLines++;
}
return skippedLines;
// 当代码运行到此位置时,file 被销毁
// 相当于 using (var file = new System.IO.StreamWriter("WriteLines2.txt")){ ... }
}

如果 using 语句中的表达式不可用,编译器将生成一个错误。

五、静态本地函数

C# 8.0中可以向本地函数添加 static 修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。 若引用了就会生成报错:CS8421-“静态本地函数不能包含对 <variable> 的引用”。

// 本地方法 LocalFunction 访问了方法 M() 这个封闭空间的变量 y
// 因此,不能用 static 修饰符来声明
int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}
// Add 方法可以是静态的,因为它不访问封闭范围内的任何变量
int M()
{
int y = 5;
int x = 7;
return Add(x, y);
static int Add(int left, int right) => left + right;
}

六、可处置的 ref 结构

用 ref 修饰符声明的 struct 可能无法实现任何接口,也包括接口 IDisposable

class Program
{
static void Main(string[] args)
{
using (var book = new Book())
Console.WriteLine("Hello World!");
}
}
// 错误写法
// Error CS8343 'Book': ref structs cannot implement interfaces
ref struct Book : IDisposable
{
public void Dispose()
{ }
}
// 正确写法
class Program
{
static void Main(string[] args)
{
// 根据 using 新特性,简洁的写法,默认在当前代码块结束前销毁对象 book
using var book = new Book();
// ...
}
}
ref struct Book
{
public void Dispose()
{
}
}

因此,若要能够处理 ref struct,就必须有一个可访问的 void Dispose() 方法。

此功能同样适用于 readonly ref struct 声明。

七、可为空引用类型

若要指示一个变量可能为 null,必须在类型名称后面附加 ?,以将该变量声明为可为空引用类型。否则都被视为不可为空引用类型。

对于不可为空引用类型,编译器使用流分析来确保在声明时将本地变量初始化为非 Null 值。 字段必须在构造过程中初始化。 如果没有通过调用任何可用的构造函数或通过初始化表达式来设置变量,编译器将生成警告。

此外,不能向不可为空引用类型分配一个可以为 Null 的值。

编译器使用流分析,来确保可为空引用类型的任何变量,在被访问或分配给不可为空引用类型之前,都会对其 Null 性进行检查。

八、异步流

异步流,可针对流式处理数据源建模 。 数据流经常异步检索或生成元素,因此它们为异步流式处理数据源提供了自然编程模型。

// 异步枚举,核心对象是:IAsyncEnumerable
[HttpGet("syncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProducts()
{
var products = _repository.GetProducts();
await foreach (var product in products) // 消费异步枚举,顺序取决于 IAsyncEnumerator 算法
{
if (product.IsOnSale)
yield return product;// 持续异步逐个返回,不用等全部完成
}
}

另一个实例:模拟异步抓取 html 数据

// 这是一个【相互独立的长耗时行为的集合(假设分别耗时 5,4,3,2,1s)】
static async Task Main(string[] args)
{
Console.WriteLine(DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\r\n");
await foreach (var html in FetchAllHtml()) // 默认按照任务加入的顺序输出
{
Console.WriteLine(DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\t" + $"\toutput:{html}");
}
Console.WriteLine("\r\n" + DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\t");
Console.ReadKey();
}
// 这里已经默认实现了一个 IEnumerator 枚举器: 以 for 循环加入异步任务的顺序
static async IAsyncEnumerable<string> FetchAllHtml()
{
for (int i = 5; i >= 1; i--)
{
var html = await Task.Delay(i* 1000).ContinueWith((t,i)=> $"html{i}",i); // 模拟长耗时
yield return html;
}
}

  

接着,其实五个操作是分别开始执行的,那么当耗时短的任务处理好后,能否直接输出呢?这样的话交互体验就更好了!

static async IAsyncEnumerable<string> FetchAllHtml()
{
var tasklist= new List<Task<string>>();
for (int i = 5; i >= 1; i--)
{
var t= Task.Delay(i* 1000).ContinueWith((t,i)=>$"html{i}",i);// 模拟长耗时任务
tasklist.Add(t);
}
while(tasklist.Any()) // 监控已完成的操作,立即处理
{
var tFinlish = await Task.WhenAny(tasklist);
tasklist.Remove(tFinlish);
yield return await tFinlish; // 完成即输出
}
}

以上总耗时取决于 耗时最长的那个异步任务5s。

  

  参考自:C# 8.0 宝藏好物 Async streams

九、异步可释放(IAsyncDisposable)

IAsyncDisposable 接口,提供一种用于异步释放非托管资源的机制。与之对应的就是提供同步释放非托管资源机制的接口 IDisposable

提供此类及时释放机制,可使用户执行资源密集型释放操作,从而无需长时间占用 GUI 应用程序的主线程。

同时更好的完善.NET异步编程的体验,IAsyncDisposable诞生了。它的用法与IDisposable非常的类似:

public class ExampleClass : IAsyncDisposable
{
private Stream _memoryStream = new MemoryStream();
public ExampleClass()
{ }
public async ValueTask DisposeAsync()
{
await _memoryStream.DisposeAsync();
}
}
// using 语法糖
await using var s = new ExampleClass()
{
// doing
};
// 优化 同样是对象 s 只存在于当前代码块
await using var s = new ExampleClass();
// doing

  参考于:熟悉而陌生的新朋友——IAsyncDisposable

十、索引和范围

索引和范围,为访问序列中的单个元素或范围,提供了简洁的语法。

新增了两个类型(System.Index & System.Range)和运算符(末尾运算符"^" & 范围运算符“..”)。

用例子说话吧:

var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5 "over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1 }; // 9 (or words.Length) ^0

运算实例:

Console.WriteLine($"The last word is {words[^1]}");
// “dog” // 使用 ^1 索引检索最后一个词 var quickBrownFox = words[1..4];
//“quick”、“brown”、“fox” 子范围 var lazyDog = words[^2..^0];
// “lazy”、“dog” 子范围 var allWords = words[..];
// “The”、“dog”子范围
var firstPhrase = words[..4];
// “The”、“fox”子范围
var lastPhrase = words[6..];
// “the”、“lazy”、“dog”子范围

另外可将范围声明为变量:

Range phrase = 1..4;
var text = words[phrase];

十一、 Null 合并赋值

Null 合并赋值运算符:??=

仅当左操作数计算为 null 时,才能使用运算符 ??= 将其右操作数的值分配给左操作数。

List<int> numbers = null;
int? i = null;
numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);
Console.WriteLine(string.Join(" ", numbers)); // output: 17 17
Console.WriteLine(i); // output: 17

十二、非托管构造类型

在 C# 7.3 及更低版本中,构造类型(包含至少一个类型参数的类型)不能为非托管类型。 从 C# 8.0 开始,如果构造的值类型仅包含非托管类型的字段,则该类型不受管理。

public struct Coords<T>
{
public T X;
public T Y;
}
// Coords<int> 类型为 C# 8.0 及更高版本中的非托管类型
// 与任何非托管类型一样,可以创建指向此类型的变量的指针,或针对此类型的实例在堆栈上分配内存块
Span<Coords<int>> coordinates = stackalloc[]
{
new Coords<int> { X = 0, Y = 0 },
new Coords<int> { X = 0, Y = 3 },
new Coords<int> { X = 4, Y = 0 }
};

Span 简介

  在定义中,Span 就是一个简单的值类型。它真正的价值,在于允许我们与任何类型的连续内存一起工作。

  在使用中,Span 确保了内存和数据安全,而且几乎没有开销。

  要使用 Span,需要设置开发语言为 C# 7.2 以上,并引用System.Memory到项目。

  Span 使用时,最简单的,可以把它想象成一个数组,有一个Length属性和一个允许读写的index

// 常用的一些定义、属性和方法
Span(T[] array);
Span(T[] array, int startIndex);
Span(T[] array, int startIndex, int length);
unsafe Span(void* memory, int length);
int Length { get; }
ref T this[int index] { get; set; }
Span<T> Slice(int start);
Span<T> Slice(int start, int length);
void Clear();
void Fill(T value);
void CopyTo(Span<T> destination);
bool TryCopyTo(Span<T> destination);
// 从 T[] 到 Span 的隐式转换
char[] array = new char[] { 'i', 'm', 'p', 'l', 'i', 'c', 'i', 't' };
Span<char> fromArray = array;
// 复制内存
int Parse(ReadOnlySpan<char> anyMemory);
int Copy<T>(ReadOnlySpan<T> source, Span<T> destination);

  Span 参考: 关于C# Span的一些实践

十三、嵌套表达式中的 stackalloc

从 C# 8.0 开始,如果 stackalloc 表达式的结果为 System.Span<T> 或 System.ReadOnlySpan<T> 类型,则可以在其他表达式中使用 stackalloc 表达式:

Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });
Console.WriteLine(ind); // output: 1

stackalloc 表达式简介:

  stackalloc 关键字用于不安全的代码上下文中,以便在堆栈上分配内存块。

// 关键字仅在局部变量的初始值中有效,正确写法:
int* block = stackalloc int[100];
// 错误写法:
int* block;
block = stackalloc int[100];

  由于涉及指针类型,因此 stackalloc 要求不安全上下文。

  以下代码示例计算并演示 Fibonacci 序列中的前 20 个数字。 每个数字是先前两个数字的和。 在代码中,大小足够容纳 20 个 int 类型元素的内存块是在堆栈上分配的,而不是在堆上分配的。
  该块的地址存储在 fib 指针中。 此内存不受垃圾回收的制约,因此不必将其钉住(通过使用 fixed)。 内存块的生存期受限于定义它的方法的生存期。 不能在方法返回之前释放内存。

class Test
{
static unsafe void Main()
{
const int arraySize = 20;
int* fib = stackalloc int[arraySize];
int* p = fib;
*p++ = *p++ = 1;// The sequence begins with 1, 1.
for (int i = 2; i < arraySize; ++i, ++p)
*p = p[-1] + p[-2];// Sum the previous two numbers.
for (int i = 0; i < arraySize; ++i)
Console.WriteLine(fib[i]);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/*
Output
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
*/

  不安全代码的安全性低于安全替代代码。 但是,通过使用 stackalloc 可以自动启用公共语言运行时 (CLR) 中的缓冲区溢出检测功能。 如果检测到缓冲区溢出,进程将尽快终止,以最大限度地减小执行恶意代码的机会。

  stackalloc 表达式参考:C#不安全代码和stackalloc

十四、内插逐字字符串的增强功能

内插逐字字符串中 $ 和 @ 标记的顺序可以任意安排:$@"..." 和 @$"..." 均为有效的内插逐字字符串。

在早期 C# 版本中,$ 标记必须出现在 @ 标记之前。

注:暂时整理这些,欢迎指正和补充。

C# 8.0 添加和增强的功能【基础篇】的更多相关文章

  1. C# 9.0 添加和增强的功能【基础篇】

    一.记录(record) C# 9.0 引入了记录类型. 可使用 record 关键字定义一个引用类型,以最简的方式创建不可变类型.这种类型是线程安全的,不需要进行线程同步,非常适合并行计算的数据共享 ...

  2. C# 7.0 添加和增强的功能【基础篇】

    C# 7.0 版是与 Visual Studio 2017 一起发布. 虽然该版本继承和发展了C# 6.0,但不包含编译器即服务. 一.out 变量 以前我们使用out变量必须在使用前进行声明,C# ...

  3. C# 6.0 添加和增强的功能【基础篇】

    C# 6.0 是在 visual studio 2015 中引入的.此版本更多关注了语法的改进,让代码更简洁且更具可读性,使编程更有效率,而不是和前几个版本一样增加主导性的功能. 一.静态导入 我们都 ...

  4. .NET的那些事儿(9)——C# 2.0 中用iTextSharp制作PDF(基础篇) .

    该文主要介绍如何借助iTextSharp在C# 2.0中制作PDF文件,本文的架构大致按照iTextSharp的操作文档进行翻译,如果需要查看原文,请点击一下链接:http://itextsharp. ...

  5. C# 中一些类关系的判定方法 C#中关于增强类功能的几种方式 Asp.Net Core 轻松学-多线程之取消令牌

    1.  IsAssignableFrom实例方法 判断一个类或者接口是否继承自另一个指定的类或者接口. public interface IAnimal { } public interface ID ...

  6. WCF学习之旅—WCF4.0中的简化配置功能(十五)

    六 WCF4.0中的简化配置功能 WCF4.0为了简化服务配置,提供了默认的终结点.绑定和服务行为.也就是说,在开发WCF服务程序的时候,即使我们不提供显示的 服务终结点,WCF框架也能为我们的服务提 ...

  7. 为现有图像处理程序添加读写exif的功能

    为现有图像处理程序添加读取exif的功能 exif是图片的重要参数,在使用过程中很关键的一点是exif的数据能够和图片一起存在.exif的相关功能在操作系统中就集成了,在csharp中也似乎有了实现. ...

  8. 微信小程序0.11.122100版本新功能解析

    微信小程序0.11.122100版本新功能解析   新版本就不再吐槽了,整的自己跟个愤青似的.人老了,喷不动了,把机会留给年轻人吧.下午随着新版本开放,微信居然破天荒的开放了开发者论坛.我很是担心官方 ...

  9. phpcms 移植【添加相关文章】功能

    添加相关文章功能相当有用,移植一个过来基本上可以实现比较复杂的页面内包含分类功能,做二次开发时可以省下不少力气. 用例:如果一个产品,属于一个厂家,而这个厂家是动态添加的,既不是一个分类,而是一个厂家 ...

随机推荐

  1. Ceph创建一个新集群 报错: File "/usr/bin/ceph-deploy", line 18, in..........

    [root@ceph-node1 ceph]# ceph-deploy new node1 Traceback (most recent call last): File "/usr/bin ...

  2. java学习第一天.day01

    Java的编译和运行机制 java文件编译成字节码文件后加载到java缓存中jvm Java的基本语法 1.Java语言严格区分大小写 2.一个Java源文件里可以定义多个Java类,但不能存在多个p ...

  3. 888. 公平的糖果交换--LeetCode

    来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/fair-candy-swap 著作权归领扣网络所有.商业转载请联系官方授权,非商业转载请注明出处. 假 ...

  4. [CISCN2019 华北赛区 Day1 Web2]ikun-1

    考点:JWT身份伪造.python pickle反序列化.逻辑漏洞 1.打开之后首页界面直接看到了提示信息,信息如下: 2.那就随便注册一个账号进行登录,然后购买lv6,但是未发现lv6,那就查看下一 ...

  5. 第一百篇:JS异步

    好家伙,打工人要打工,博客会更新的没有以前频繁了   芜湖,一百篇了,这篇写一个比较难的异步(其实并不难理解,主要是为promise铺垫)   老样子,先补点基础: 1.进程 来吧,新华字典    大 ...

  6. OSI模型 TCP/IP协议

    常见术语 网络相关的术语 1.拓扑:物理拓扑-----体现了设备之间的连接关系 逻辑拓扑----设备之间的通信关系 2.数据载荷:传递的实际信息 3.报文(PDU--协议数据单元) 4.数据头部的作用 ...

  7. KingbaseES的SQL语句-CTE递归

    背景 从上下级关系表中,任意一个节点数据出发,可以获得该节点的上级或下级.CTE的递归语法,或者 connect by 与 start with的 查询语法,能够实现这个需求. 当我们需要制作上下级关 ...

  8. 阿里云CentOS7安装K8S

    1. 在阿里云山申请三台云服务器 1.1 环境准备 完成配置后的信息 服务器IP 操作系统 CPU 内存 硬盘 主机名 节点角色 172.18.119.145 centos7 2 4G 50G k8s ...

  9. 理解 Spring IoC 容器

    控制反转与大家熟知的依赖注入同理, 这是通过依赖注入对象的过程. 创建 Bean 后, 依赖的对象由控制反转容器通过构造参数 工厂方法参数或者属性注入. 创建过程相对于普通创建对象的过程是反向, 称之 ...

  10. windows清理必看

    清理缓存 代码如下 介绍此文件夹都是缓存文件全选删除即可 ctrl+A全选shift+del强制删除(不会添加到回收站) %temp% 找到C盘右击属性选择想要删除的文件进行清理即可 清理完点击清理系 ...