C#笔记 其零
C#语言基础
数据类型
值类型
符号 | 描述 | BCL名(不使用) | 后缀(用大写) |
---|---|---|---|
sbyte | 8-bit signed | System.SByte | |
byte | 8-bit unsigned | System.Byte | |
short | 16-bit signed | System.Int16 | |
ushort | 16-bit unsigned | System.UInt16 | |
int | 32-bit signed | System.Int32 | |
uint | 32-bit unsigned | System.UInt32 | U u |
long | 64-bit signed | System.Int64 | L l |
ulong | 64-bit unsigned | System.UInt64 | UL ul |
float | 32-bit 7位有效数字 | System.Single | F f |
double | 64-bit 15~16位有效数字 | System.Double | D d |
decimal | 128-bit 28~29位有效数字 能保持精度的特殊浮点类型 | System.Decimal | M m |
bool | 8-bit | System.Boolean | |
char | 16-bit Unicode编码 | System.Char |
事实上,简单类型(以上类型)在C#中实现为struct
数字写法
- 下划线间隔
9_455_533U
- 指数计数法
60.0615E23
- 其他进制
0x123
0b1101
浮点类型
float
和 double
有 0f/0
得 NaN
(Not a Number), 1f/0
得 Infinity
或 ∞
避免在比较中使用float和double这两种二进制浮点类型(
double
==float
等),要么使用十进制浮点类型decimal
,要么判断两数之差在容差范围内。
布尔类型
在C#语言中,预定义类型不能显示或隐式转换为 bool
型
if
for
while
语句中必须使用 bool
型
int i = 12;
if(i) ...; // Error
重载了 true/false
和 operator bool
操作符的类可以在 if
for
while
语句中使用,详见良好构建
引用类型
string 字符串
C#的字符串使用Unicode字符
-
构建
string s = "123"; s = new string('c', 4); // "cccc"
-
比较
Write(string.Compare("abc", "ABC")); Write(string.Compare("abc", "ABC", true)); // 忽略大小写 // string.Compare返回-1, 0, 1三种值 bool a = "abc" == "Abc"; a = "abc".Equals("ABC");
一般来说,引用类型的
==
操作符比较的是引用(地址)是否相等,但string
类型比较的是引用的值是否相等 -
其他方法
// 用法看函数名就知道了 string s = "abcABC"; // 因为string是不可变的,所以以下方法都是有返回值的,可变的是System.Text.StringBuilder s.Contains("abc"); s.IndexOf("cAB"); s.LastIndexOf("bcA"); s.IndexOfAny(new char[]{'c', '#', '?'}); s.Substring(2, 5); s.Insert(2, "<>?"); s.Remove(2, 5); s.Replace("bc", ""); s.TrimStart(); s.TrimEnd(); s.Trim(); string.Join(",", new string[] {"abc", "123"}); s.Split(new char[] {' ', ','}); s.ToUpper(); s.ToLower();
-
插值字符串
$"12{arg}3abc"
,arg
为上文变量常量名 -
原生字符串
@"\n\t465"" "
-
结合
$@"\{arg}""\"
,$
在@
前Windows: CRLF "\r\n"
Unix: LF "\n"
使用System.Environment.NewLine换行
object类型
object ob = 12;
System.Console.Write(ob);
ob = "8465"; // 可存储不同类型
System.Console.Write(ob);
int val = 45;
object obj = val; // boxing
val = (int)obj; // unboxing
tuple 元组
(string country, string capital, double GDP) = ("England", "London", 2829.11);
System.Console.WriteLine($"{capital} {country}: {GDP}");
var (country, capital, GDP) = ("England", "London", 2829.11);
System.Console.WriteLine($"{capital} {country}: {GDP}");
var countryInfo = (Country: "England", Capital: "London", GDP: 2829.11);
System.Console.WriteLine($"{countryInfo.Capital} {countryInfo.Country}: {countryInfo.GDP}");
System.Console.WriteLine($"{countryInfo.Item1} {countryInfo.Item2}: {countryInfo.Item3}");
var countryInfo = ("England", "London", 2829.11);
System.Console.WriteLine($"{countryInfo.Item1} {countryInfo.Item2}: {countryInfo.Item3}");
数组
-
定义
string[] text; // one dimension int[,] cells; // two dimensions // 只声明是无法使用的 string langs = {"C", "Python", "Csharp", "Java"}; // 声明时同时赋值 text = {"one", "second", "the"}; // error: 声明和赋值分开的话要用new text = new [] {"one", "second", "the"}; cells = new int[3,4]; int arr = new int[4] { 1, 2, 3 }; // error: []内数字因与{}内数量一样, 4 != len({1, 2, 3}) System.Console.Write(cells[1, 2]); int[][] narr = { new int[]{1,2,3,4}, new int[4], // 注意全初始化为default(int) {1,2,3} // error: 必须由new创建 } // 交错数组 或 数组的数组 int[][] narr; narr = new int[2/*与后面new的个数相同相同*/][/*不填*/] { new int[] { 1, 2, 3, 4 }, new int[4] }; System.Console.Write(narr[0][2]);
-
改变大小
int[] arr = new int[20]; Array.Resize(ref arr, 45);
-
方法
// arr会被改变 arr.Sort(); arr.Sort(x => Math.Abs(x)); Array.Reverse(arr);
其他方法可以见集合和(集合)[https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/collections]
初始值
每种类型都有对应的初始值,可以通过 default(type)
查看
int a = default(int);
int b = default; // both are ok
类型转换
-
显式
long b = 12; int a = (int)b
-
隐式
long b; int a = 12; b = a
-
System.Convert
double d = System.Convert.ToDouble("123.2"); bool b = true; string s = b.ToString(); // s == "True"
-
Parse和TryParse
int a = int.Parse("12"); // 无法转换引发异常 int.TryParse("12", out a); // 无法转换返回false,转换成功时将值存在a中 int.TryParse("45", out _); // 舍弃
-
unchecked(慎重使用)
unchecked { uint a = (int)-1; }
-
CLR技巧(慎重使用)
uint[] arr = (uint[])(System.Array)new int[]{ -1, -2, -3 };
不同的CLR不一定都能实现此技巧
-
枚举类型转换
见枚举
其他类型
可空类型
int? i = null; // 可以标记值是否为null
i.HasValue; // 返回i是否为null,即 i != null
i.Value; // 若i不为空,返回i的值
System.Console.WriteLine(i.HasValue ? i.Value : 0);
System.Console.WriteLine(i ?? 0);
// 实际上是一种语法糖
Null<T> t = new Nullable<T>();
T? t = new T();
// 二者等价
隐式类型和匿名类型
var s = "ads".ToUpper();
// 类型由编译器推断,类似C++的auto关键词
// JavaScript的var和VB的Variant则类似于C#的object
System.Console.WriteLine(s);
Comparison: auto in C++ and var in C#
var i = 10; var info = ("abc", 456, 3.14F); // 支持元组
auto i = 10; auto foo() -> int { return 5; } auto bar() -> decltype(printf) { } // 尾置返回 template<typename T, typename U> auto add(T t, U u) { return t + u; }
auto的用法更加广泛,而var只能用在变量声明时
auto是C/C++原有的关键字,但几乎只有编译器作者才会使用,使用auto而不使用var可做到:
- 不新增关键字
减少设定新关键字的工作,也能兼顾使用了var作变量名的旧代码,还能兼容C- 更贴切
在多种无类型语言、解释型语言和动态语言中,var一般用作无类型变量声明
流程控制
基本流程控制语句
语法结构 | 特性 |
---|---|
if (boolean-expression) { operation } else { operation } |
嵌套(连贯): if() {} else if() {} else if() {} else if() {} else {} |
while (boolean-expression) { operation } |
改尾递归为迭代 称一次循环为一次迭代 |
do { operation } while (boolean-expression) |
至少执行一次 常用于提示用户输入 |
for (initializer; boolean-expressing; iterator) { operation } |
init -> bool -> body(out) -> iter -> bool (loop<<-) |
foreach (type variable in collections) { operation } |
type是collections中每一项的数据类型,可用var。 循环期间禁止修改变量(const) |
switch(type-expression) { case const-expression: operation jump-out (more cases) default: operation jump-out } |
case和default及后续语句称为switch小节 每个小节的结尾必须是break或return或goto(不能贯穿) |
continue; break; |
|
goto identifier; goto case const-expression; goto default; |
switch中可用goto case/default跳转到其他小节 出现过多这种情况,请重构以避免使用过多goto |
特殊操作符
cond ? true-state : false-state // cond为true,等价于true-state,否则为false-state true-state和false-state类型相同
expression1 ?? expression2 // 等同于 expression1 != null ? expression1 : expression2 但expressions1并不会计算两次
expression1 ?? expression2 ?? expression3
args?.Attr // 等同于 args != null ? (typeof(args.Attr)?)args.Attr : null 但args并不会计算两次
args?.Attr ?? false
异常处理
try/catch/finally
void F(int val) {
try {
Foo();
throw new ArgumentException("invalid value", nameof(val));
// 此时Foo()会被执行Bar()不会
Bar();
}
catch (Win32Exception e)
when (e.NativeErrorCode == 42) /* when后可以加异常条件表达式 */ {
// Handle Error
}
catch (ArgumentException e) {
System.Console.WriteLine(e.Message);
}
catch (Exception e /* Exception是所有异常的基类 */ ) {
throw; //使用空throw可以保存Error的栈信息,将Error再次抛出
throw e; //如果使用这种方法会使得Error的信息被改变,不推荐使用
}
catch () {} // Error: Exception是最宽泛的异常类了
finally {
// 无论是否发生异常都会到达的块,常用于释放资源
}
}
如果用户输入非法参数,使用通常的流程处理进行处理和返回信息而非通过异常处理进行处理和返回信息
越宽泛的异常越放在后面, System.Exception
是最宽泛的异常所以应该放在最后,可以用 catch {}
代替
事实上,
catch {}
是catch (object) {}
,但不能直接写成后者
规范
- 只捕捉能处理的异常
- 不隐藏不能处理的异常
- 少用
System.Exception
和catch {}
捕捉异常 - 不在调用栈较低的位置报告或记录异常,允许异常在调用栈上向上传播
- 使用
throw;
而不是throw e;
- 想好异常条件避免在
catch
中抛出异常 - 避免在异常条件表达式中抛出异常
- 避免以后可能变化的异常条件表达式
- 重写抛出异常时要小心
方法
命名空间
namespace.class.function
几乎所有C#程序都要用System命名空间
using System; // 引入命名空间
using static System.Console; // 加static引入类和方法
using Csl = System.Console; // 使用别名
// 引入后即可不加前缀(除非名字冲突),但为清楚命名空间和类,本文使用全称
using
也可在命名空间中使用,但不应该这样使用
声明和定义方法
一般来说,C#不区分声明和定义,除非使用名为分部方法的高级功能(查看分布类部分)
class A{
public static void Foo(){
// 可被外部访问,静态,无返回值
}
private static int Bar(){
// 不可被外部访问,静态,返回int值
}
public string Baz(int a){
// 可被外部访问,非静态,返回string值
}
private (string Country, double GDP) Qux(){
// 不可被外部访问,非静态,返回元组
return ("London", 1.123);
}
private string Fun(string s1, string s2) => $"{s1} {s2}"; // 简化表达式主体方法
}
参数
引用参数
static void Main() {
int x = 0;
Foo(x); // x不变
FooRef(ref x);
FooOut(out x); // 可以使用 FooOut(out int y) 直接赋值y
FooIn(in x);
// ref, out, in传递的都是引用
int[] arr = {1, 2, 3, 4};
Foo(arr);
// arr[3] == 12
}
static void Foo(int x) { ++x; }
static void FooRef(ref int x) { ++x; }
static void FooOut(out int x) { x = 12; } // 必须赋值x否则报错
static void FooIN(in int x) { System.Console.WriteLine(x); } // x只读
static void FooArr(int[] x) { x[x.Length-1] = 12; } // 传递引用类型
static void F()
{
int[] a = { 1, 2, 3 };
int[] b = { 4, 5, 6 };
Swap(ref a, ref b); // 交换数组
}
public static void Swap<T>(ref T a, ref T b)
{
T t = a; a = b; b = t;
}
参数数组
static void Main(){
Fun("one", "adsj", "532");
string[] t = {"one", "adsj", "532"};
Fun(t);
Fun(); // empty is Ok
}
static void Fun(params string[] strs){
// params参数只能放在最后,若想保证最少参数数量可以写成(string, params string[])
foreach (string s in strs) System.Console.WriteLine(s);
}
可选参数和具名参数
static void F(string name = "Andy", int seq = 1) => System.Console.Write($"{name}: {seq}");
static void A() => F();
static void B() => F(seq: 5, name: "Kid");
返回引用
// 所有的ref都不可缺少
public static ref int F(int[] arr){
return ref arr[arr.Length-1];
}
static void A(){
int[] arr = {1, 2, 3};
ref int val = ref F(arr); // val即arr[2]
int a = 12;
ref int b = ref a;
// ref局部变量不能修改,不能用字面值初始化,必须立即初始化
}
class A {
ref string s; // error: 不允许声明ref字段
string str;
public ref string Str { // error: 不允许把自动属性声明为引用类型
get{ return str; }
set{ str = value; }
}
public ref string GetStr(){
return ref str; // correct: 允许返回引用的属性
}
}
重载
操作性多态的实现
void Foo(string s) {}
void Foo(string s, int a=0) {} // 错误:使用可选参数时无法与上式区分
void Foo() => Foo(string.Empty); // 将实现主体转移到Foo(string)中,另类的可选参数
void Bar(int a) {}
int Bar(int a) {} // 错误:不能只靠返回值区分方法
方法的唯一性通过方法名、参数类型和参数数量来确定
迭代和递归
递归意味着方法调用自身,反之为迭代
可以用while改写尾递归
用stack改写头递归和中间递归
递归实现方法:
F(x) {
if (reach the end)
return;
else {
breakdown x into small ones
F(smaller x); // use F(x) as if F(x) has been able to solve questions
}
}
Main方法
命令行参数和返回值
$ .\Program -a.cfg -b.log # 调用可执行文件
class Program{
static int Main(string[] args){
foreach (string arg in args)
System.Console.WriteLine(arg);
/*
-a
.cfg
-b
.log
*/
return 1; // 非0值代表非正常退出(此处示例),交由操作系统或其他程序判断
}
}
$ echo $? # False
规范
System.Console.WriteLine($"Hello, {0}!", System.Console.ReadLine());
// which is better?
string name = System.Console.ReadLine();
System.Console.WriteLine($"Hello, {0}!", name);
通常情况下,Readability胜于Short
用动词或动词短语按PascalCase作方法名