学习 C# Part 1——基础类型,控制流,函数,集合类型

学一下 C#,参考 Learn X in Y minutes 的形式,后面的部分可能遥遥无期了,混个脸熟先。

基础

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 注释的形式同 Java
/*
以及 Scala 等各种 C 系语言
文档注释形式后面另说
*/

// 打印一行
Console.WriteLine("Hello, World");

// 基本类型包括 byte, short, int, long, double, float, decimal, bool, char
// 其中,除 byte 之外所有数字类型都为有符号,byte 默认无符号
// 因此另外提供了 sbyte,表示有符号(signed)byte
// 其它数字类型提供 uXXX,比如 uint,ulong,表示无符号(unsigned)类型
byte fooByte = 1;
short fooShort = 32767; // 2^15 - 1
long fooLong = 9223372036854775807L; // 使用 L 后缀

// decimal 是 128 位浮点数,使用 m 后缀(就如 float 使用 f 后缀)
decimal fooDecimal = 100.00000123123m;

bool fooBool = true; // or false
char a = 'c';

// 数学运算如 java
Console.WriteLine(1 + 2);
Console.WriteLine(42 != 42);
Console.WriteLine(3 / 2);
Console.WriteLine(1 << 2);
Console.WriteLine(true ? 1 : 2)

// 使用 var 让编译器去推断变量类型,var 在某些时候是必须的,当类型没有名字的时候
var someInt = 1;
var someString = "rrr";

// 字符串类型 string,同 java 的一样,string 也是引用类型,且不可变。
string foolString = "hello \t world";
// 多行字符串的形式,使用@前缀,内容中使用""来表示"
string multipleLineString = @"
# Hello
Me: ""Hello""!
";

// 字符串重载了 [],可以像 js 和 C++一样通过下标访问字符串特定索引的字符
char foolChar = foolString[0]; // 'h'

// 元组类型也是非常重要的,它形式和 kotlin 以及 scala 的一样,且能够进行解构
var (item1, item2, item3) = (1, 2, 3);
var somePair = (1, 2);
// 字符串
string stringA = "abc";
string stringB = "abc";
Console.WriteLine(stringA == stringB); // True,
// 注意! ==的默认行为同 Java,但是可以被重载!

// 字符串的比较使用 string.Compare(一个静态方法)
string.Compare("hello", "HELLO", StringComparison.OrdinalIgnoreCase);

// 格式化
string helloStr = string.Format("Hello, {0}", "World");
// 但既然有模板字符串,还用啥格式化呢?
string name = "Yuuki";
string helloStr1 = $"Hello, ${name}";
// 日期以及格式化
DateTime fooDate = DateTime.Now;
Console.WriteLine(fooDate.ToString("hh:mm, dd MMM yyyy"));

// 控制结构,如 if,for,while,do-while,switch 语法同 java 一致
if (42 == 42)
{
Console.WriteLine("correct!");
}
else
{
Console.WriteLine("never!");
}

var i = 0;
while (i++ < 10)
{
Console.WriteLine("rrr");
}

for (int j = 0; j < 10; j++)
{
Console.WriteLine("rrr");
}

// 高阶 for 循环有单独的关键字 foreach
foreach (var elem in new int[]{ 1, 2, 3 }) // 数组字面量,这里的类型 int 可以省去,编译器可以推断
{
Console.WriteLine(elem);
}

// 类型转换方法在 Convert 类,以及各个基本类型的方法中
Console.WriteLine(int.Parse("123"));
Console.WriteLine(Convert.ToInt32("123"));

函数定义,Lambda,柯里化

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// 函数定义,语法同 Java,注意首字母大写(方法也如此),且花括号换行
void Hello(string name)
{
Console.Write("Hello, {0}", name); // printf
}
Hello("Yuuki"); // 函数调用

// 默认值参数,和不定参数
void someFn(string name, int age = 18, params string[] otherParams) {}
void manyArgFn(string name, int age, string clazz = "765", string description = "") {}

// 调用时通过名称来传递参数以跳过默认值参数
manyArgFn("haruka", 18, description: "TOP IDOL!");

// 函数的参数还能够使用 ref 和 out 去修饰
// ref 表示传递引用(就如 c++的&)
void inc(ref int n)
{
n = n + 1;
}
int i = 0;
inc(ref i); // 调用时必须给定 ref 关键字表示传递引用
Console.WriteLine(i); // 1

// out 关键字可以认为是一个只写的引用,用来传递函数调用的结果到一个变量中
// C#提供的一些类型转换操作使用的就是这种方式,如 int.TryParse,这应该是 C#没有元组的时代返回多个值的处理方式
void TryParseInt(string str, out int result)
{
try
{
var i1 = int.Parse(str);
result = i1;
}
catch (Exception e)
{
result = int.MinValue;
}
}

int result;
TryParseInt("123", out result);
Console.WriteLine(result);

// 也可以即席定义 out 输出的变量……WTF?
TryParseInt("234", out var result1);
Console.WriteLine(result1);

// 函数类型如 Java 的函数式接口,这里用了泛型语法,和 Java 的一致
// int => int,或者说 Function<Integer, Integer>
Func<int, int> doubleFn = n => { return n * 2; }; // 或者 n => n * 2

// 函数变量可以直接调用,这是重载了 () 吗?
Console.WriteLine(doubleFn(3));

// (int, int) => int,或者说 BiFunction<Integer, Integer, Integer>
Func<int, int, int> plus = (x, y) => x + y;

// () => int, 或者说 Supplier<Integer>
Func<int> supplier = () => 42;

// int => void, 或者说 Consumer<Integer>
Action<int> consumer = (int n) => Console.Write("me a {0}", n);

// 方法引用,直接取名字即可
Action<string> print = Console.Write;

// 函数的参数是逆变的,返回值是协变的,这是说——
// 考虑一个函数 object => string
Func<object, string> fn0 = obj => "42";

// 因为参数是逆变的,且 string 是 object 的子类型,因此 object => string 是 string => string 的子类型
// 因此,可以把 object => string 对象赋值给 string => string 类型变量:
Func<string, string> fn1 = fn0;

// 因为返回值是协变的,且 string 是 object 的子类型,因此 object => string 是 object => object 的子类型
// 因此,可以把 object => string 对象赋值给 object => object 类型变量:
Func<object, object> fn2 = fn0;

// 简单来说,入参可以更狭窄,返回值可以更广泛:
Func<string, object> fn3 = fn0;

// 闭包没有任何限制:
Func<Func<int>> Counter = () =>
{
int count = 0;
return () => count++;
};
var fooCounter = Counter();
Console.WriteLine(fooCounter());

// 函数变量也可以递归,只要能够容忍警告(建议还是定义函数)
Func<int, int> fib = null;
fib = x => x <= 1 ? x : fib(x - 1) + fib(x - 2);
Console.WriteLine(fib(10));

// 柯里化形式和 Java 的一样,但调用的时候更舒服一些
Func<int, Func<int, Func<int, int>>> xPyPz = x => y => z => x + y + z;
Console.WriteLine(xPyPz(1)(2)(3));

集合类型的使用

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
// 数组定义和使用方式和 java 一致

using System.Collections;

int[] arr0 = new int[]{};
int[] arr1 = new int[3] { 1, 2, 3 };
int[] arr2 = {1, 2, 3};

string[] strArr = {"hello", "world"};
// 数组是协变的,但编译器会给出警告
object[] objArr = strArr;

// 多维数组第一印象可以这么定义
int [][] multiDArr = {new []{0,1,2}, new []{2,3,4,5,6}};

// 但对于任意维度等长的多维数组,C#提供了原生的方式:
int[,] multiDArr1 = { { 0, 1, 2 }, { 3, 4, 5 } };
// 访问其只需要一个 []:
Console.WriteLine(multiDArr1[0, 1]); // 1

// 但什么年代了,谁还用数组呢?
// C# 也有一套完整的集合类型,其中包括带泛型和不带泛型各一套,这里只考虑带泛型的版本

// 对于列表,包括 List<T>(对应 Java 的 ArrayList<T>),Queue<T>,Stack<T>,SortedList<T>,LinkedList<T>,其均和 Java 的类似
// 但不像 Java,并没有像 List,Map 这样大而全的接口来作为基类
var a = new List<int>();
a.Add(1);
a.Add(2);
foreach (var i in a)
{
Console.WriteLine(i);
}

// 列表可以用对象初始化器语法构造,其将被编译成一连串 Add 调用(如何实现的?)
var b = new List<int>
{
1, 2, 3
};

// 然后是 Set 和 Map,在 C#中 Set 包括 HashSet,SortedSet,Map 包括 Dictionary,SortedDictionary,容易发现和 Java 版本的对应关系
// 同样可以使用对象初始化器构造,其都分别调用各自的 Add 方法
var someSet = new HashSet<int>
{
1, 2, 3
};
// Map 的 Add 方法有两个参数 key 和 value,使用数组的形式去包裹
var someMap = new Dictionary<int, string>
{
{1, "hello"},
{2, "world"}
};

map-filter-reduce

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
// IEnumerable 接口中提供了 map-filter-reduce 等方法,使 C#对函数式编程有很舒服的支持(更酷炫的在后面)
// 并且,这些操作均是惰性的,也就是说其将有同 Java 的 Stream 一样的性能
// Map 和数组也继承了该接口
var lst = new List<int>
{
1, 2, 3, 4, 5
};
var map = new Dictionary<int, string>
{
{1, "hello"},
{2, "world"},
};

// map 即 Select 方法,对每个元素进行同样操作
var lstPlus1 = lst.Select(i => i + 1).ToList(); // [2, 3, 4, 5, 6]
var keys = map.Select(pair => // [1, 2]
{
// 解构语法!
var (key, value) = pair;
return key;
}).ToList();

// flatMap 即 SelectMany 方法,对每个元素进行同样操作得到一个集合,将所有集合展平作为结果
var doubleList = lst.SelectMany(i => new[] { i, i }).ToList(); // [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]

// filter 即 Where 方法,对集合进行筛选,筛选出满足条件的新元素组成集合
var oddList = lst.Where(i => i % 2 == 1).ToList(); // [1, 3, 5]

// reduce 即 Aggregate 方法,顾名思义,将整个集合折叠成一个值,C#似乎只有左折叠
var lstSum = lst.Aggregate(0, (acc, x) => acc + x); // 15

LINQ 的使用

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
// LINQ,C#提供的一种类似 SQL 的对数组,数据库,以及其它种类数据进行查询的方式
// LINQ 其本质类似 Scala 的 for 语句,或 Haskell 的 do 语句,也就是说,它能够用来操作 Monad
// 这里只学习能使用 flatMap 以及 map 描述的部分
var nums = new [] {1, 2, 3};
// 基本使用类似于:
IEnumerable<int> someQuery0 =
from num in nums
where num % 2 == 0
select num + 1;
// 其对应下面语句:
var someQuery1 =
nums.Where(num => num % 2 == 0) // 或者仅用一个 SelectMany 描述
.Select(num => num + 1);

// 当然,若仅能操作单个集合,那就有点弱智了,我们可以给两个数组做个 join:
var nums2 = new[] { 4, 5, 6 };
var someQuery2 =
from num1 in nums
from num2 in nums2
where (num1 + num2) % 2 == 0
select (num1, num2);
// 等价于:
var someQuery3 =
nums.SelectMany(num1 =>
nums2.Where(num2 => (num1 + num2) % 2 == 0) // 或者用一个 SelectMany 描述
.Select(num2 => (num1, num2)));

// 但 LINQ 的功能更多,它还有 join,order by,group by 等语法,这就无法单纯使用 flatMap 去描述了,这里先不表
// 但应该都可以描述成为两个/多个集合做笛卡尔积后进行各种操作

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!