第一个C# 程序

那么常规操作,第一步打印Hello World

1
2
3
4
5
6
7
8
9
10
11
12
using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}

这个写法是不使用顶级语句的,在C# 9.0引入了顶级语句,可以省略类和Main方法,直接写代码

注释

接下来看注释,注释与Java类似,分为单行注释和多行注释

//单行注释

/* ... */多行注释

///用于注释类、方法、属性等,可以自动生成文档,这个是C#特有的,在类、方法上面敲入三个斜杠,即可自动生成注释模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 命名空间引用
using System;

// 命名空间定义(解决方案的名字)
namespace ConsoleApp1
{
// 类定义(类名)
class Program
{
/// <summary>
/// 程序入口
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
// 打印Hello, World!
Console.WriteLine("Hello, World!");
}
}
}

控制台相关内容

现在我们主要编写控制台应用,所有得来了解了解控制台相关的东西

输入输出

WriteLine输出并自动换行

Write输出但不换行

ReadLine获取输入,并自动换行,直到用户按下回车键

ReadKey获取输入,用户按下任意键就继续执行

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
using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
// 打印Hello, World!(并自动空行)
Console.WriteLine("Hello, World!");

// 打印Hello(不自动空行)
Console.Write("Hello");

// 获取输入
Console.ReadLine();
Console.WriteLine("输入完毕");

// 获取输入(这个是按任意键就继续执行)
Console.ReadKey();

Console.WriteLine("按了任意键");
}
}
}

获取输入但不显示

如果你不希望用户输入的内容显示在控制台上,可以使用ReadKey(true),这样用户输入的内容就不会显示在控制台上,而是存储在KeyChar属性中,返回为char类型

1
2
char c = Console.ReadKey(true).KeyChar; // 获取用户输入的字符,不显示在控制台上
Console.WriteLine("用户输入的字符是:" + c); // 输出用户输入的字符

清空控制台

Clear清空控制台

1
Console.Clear();

控制台窗口

设置控制台大小

SetWindowSize设置控制台大小,参数为宽和高,单位为字符

1
Console.SetWindowSize(80, 30);

设置控制台缓冲区大小

SetBufferSize设置控制台缓冲区大小,参数为宽和高,单位为字符

注意,缓冲区大小不能小于控制台大小,否则会报错

1
Console.SetBufferSize(80, 30);

退出控制台

Exit退出控制台

1
Console.Exit(0);

光标

设置光标位置

窗口中,左上角坐标为(0,0),向右为X轴正方向,向下为Y轴正方向

SetCursorPosition设置光标位置,参数为横坐标和纵坐标,单位为字符

1
Console.SetCursorPosition(10, 5); 

在视觉上,1y = 2x,所以要想让光标起始位置看起来像个正方形,就得将x坐标设置为y坐标的两倍

隐藏光标

CursorVisible设置光标是否可见,参数为布尔值

1
Console.CursorVisible = false;

颜色

文字颜色

ForegroundColor设置文字颜色,参数为枚举类型ConsoleColor,默认为白色

1
Console.ForegroundColor = ConsoleColor.Red;

背景颜色

BackgroundColor设置背景颜色,参数为枚举类型ConsoleColor,默认为黑色

注意设置好背景颜色之后,需要Clear一次,才能看到效果

1
2
Console.BackgroundColor = ConsoleColor.Blue;
Console.Clear();

折叠代码

与直接折叠大括号{}直接的代码不同,C#提供了#region#endregion来折叠代码

你可以在代码中直接使用它们来自定义你要折叠的代码块

#region开头,以#endregion结尾,可以折叠代码,方便阅读

执行编译时,#region#endregion会被忽略,不会影响代码的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
#region Region1
Console.WriteLine("Hello, World!");
#endregion
// 上面部分可以折叠为Region1
}
}
}

数据类型

好,接下来我们来看看C#的数据类型

有符号整型

有符号整型是可正可负的

sbyte -128~127,占用1字节

short -32768~32767,占用2字节

int -2147483648~2147483647,占用4字节

long -9223372036854775808~9223372036854775807,占用8字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;

namespace LS2
{
class Program
{
static void Main(string[] args)
{
// 有符号的整型sbyte short int long
sbyte a = 100; // -128~127
short b = 100; // -32768~32767
int c = 100; // -2147483648~2147483647
long d = 100; // -9223372036854775808~9223372036854775807
}
}
}

无符号整型

那么顾名思义,它就是非负的整型

不过它所代表的数值的数量是和有符号整型一样的

byte 0~255,占用1字节

ushort 0~65535,占用2字节

uint 0~4294967295,占用4字节

ulong 0~18446744073709551615,占用8字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;

namespace LS2
{
class Program
{
static void Main(string[] args)
{
// 无符号的整型byte ushort uint ulong
byte e = 100; // 0~255
ushort f = 100; // 0~65535
uint g = 100; // 0~4294967295
ulong h = 100; // 0~18446744073709551615
}
}
}

浮点型

浮点数,即小数,它分为单精度和双精度

float 7~8位有效数字(有效数字:从第一位非零数字起算),需加f,占用4字节

double 15~16位有效数字,写小数时,默认为此类型,其他两个的需要指定,占用8字节

decimal 28~29位有效数字,需加m,占用16字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

namespace LS2
{
class Program
{
static void Main(string[] args)
{
// 浮点型 float double decimal
float i = 100.0f; // 7~8位有效数字
double j = 100.0; // 15~17位有效数字
decimal k = 100.0m; // 28~29位有效数字
}
}
}

字符型

char 只能存储一个字符,只能用单引号括起来,占用2字节

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

namespace LS2
{
class Program
{
static void Main(string[] args)
{
// 字符
char n = 'a'; // 只能存储一个字符
}
}
}

布尔型

bool true/false,占用1字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

namespace LS2
{
class Program
{
static void Main(string[] args)
{
// 布尔
bool l = true;
bool m = false;
}
}
}

字符串

字符串类型的,一般用双引号括起来,可以存放多个字符,没有固定的占用字节数,根据内含字符的数量来决定的

string 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

namespace LS2
{
class Program
{
static void Main(string[] args)
{
// 字符串
string o = "hello world"; // 可以存储多个字符
}
}
}

变量初始化

一般来说,变量声明后就得初始化;亦可先定义,再初始化

1
int a = 100;

先定义,再初始化

1
2
int a;
a = 100;

但不论如何,使用变量之前必须初始化

另外,可以连续声明相同类型的变量,用逗号隔开

1
int a = 100, b = 200, c = 300;

变量命名规范

  1. 变量名必须以字母、下划线开头
  2. 变量名只能包含字母、数字、下划线,并区分大小写,不能重复
  3. 变量名不能使用关键字
  4. 变量名应当顾名思义
  5. 一般遵循驼峰命名法,即首字母小写,后续每个单词首字母大写(函数、类可采用大驼峰命名法)

常量

常量,即不可改变的量,用const关键字定义,必须初始化,不能修改

1
2
const int months = 12;
Console.WriteLine("一年有{0}个月", months);

转义字符

转义字符,即用反斜杠\开头的字符,用于表示一些特殊字符

\n 换行
\t 制表符
\\ 斜杠
\" 双引号
\' 单引号
\b 光标退格
\0 空字符
\a 警报音

1
2
3
4
5
6
7
8
Console.WriteLine("换行符\n换行");
Console.WriteLine("制表符\t123");
Console.WriteLine("斜杠\\123");
Console.WriteLine("\"双引号\"");
Console.WriteLine("\'单引号\'");
Console.WriteLine("光标退格123\b123");
Console.WriteLine("空字符\0123");
Console.WriteLine("警报音\a");

类型转换

隐式转换

隐式转换为程序帮助我们转换,而不用我们手动转换

值得注意的是,数据类型——大范围装小范围

1
2
3
int a = 10;
long b = a; // 隐式转换
// short c = a; // 不能隐式转换,因为short范围 < int范围

在上面的例子中,a可以隐式转换为long类型的

a隐式转换为short类型的,因为short的范围小于int的范围

同样的,还有以下注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// decimal不能隐式转换double和float,但整型可以被转换
double d = 10.0;
// decimal e = d; // 不能隐式转换

// 有符号数不能隐式转换为无符号数
// uint f = a; // uint是无符号整数,int是有符号整数,不能隐式转换

// 无符号数隐式转换为有符号数,要求无符号数的范围必须小于有符号数的范围
uint g = 10;
long h = g; // 可以隐式转换,因为uint范围 < long范围

// 所有整型都可隐式转换为float、double
float i = a;
double j = b;

// string类型和bool型不能和其他类型隐式转换
// bool k = a; // 不能隐式转换

// 其他类型不能隐式转换为char,但char可以隐式转换为其他类型(string,小于int范围的也不能转换)
// char l = a; // 不能隐式转换
char m = 'a';
int n = m; // 其实是转换为ASCII码值

显式转换

显示转为为强制转换,用括号()

变量类型 变量名 = (目标类型)变量名

但要注意的是,强制转换可能导致溢出的问题,需要注意转换类型之间的范围

同时,其精度也有可能丢失

1
2
int o = 10;
short p = (short)o; // 显式转换

变量类型.Parse()方法可以将字符串转换为其他类型

1
2
3
4
5
int q = int.Parse("123");
// int r = int.Parse("123.45"); // 会报错,因为字符串中包含小数点

short s = short.Parse("123");
double t = double.Parse("123.45");

Convert.ToXXX()方法可以更准确地转换变量类型

每一种类型都有对应的Convert.ToXXX()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int u = Convert.ToInt32("123");
int v = Convert.ToInt32(123.45);

sbyte w = Convert.ToSByte("123");
short x = Convert.ToInt16("123");
int y = Convert.ToInt32("123");
long z = Convert.ToInt64("123");

byte aa = Convert.ToByte("123");
ushort bb = Convert.ToUInt16("123");
uint cc = Convert.ToUInt32("123");
ulong dd = Convert.ToUInt64("123");

float ee = Convert.ToSingle("123.45");
double ff = Convert.ToDouble("123.45");
decimal gg = Convert.ToDecimal("123.45");

bool hh = Convert.ToBoolean("true");
char ii = Convert.ToChar("a");

string jj = Convert.ToString(123);

所有类型都可以通过ToString()方法转换为字符串

1
2
int a1 = 123;
Console.WriteLine(a1.ToString());

另外,补充说明以下,Console.ReadLine()方法会读取用户输入的字符串,并返回一个string类型

异常捕获

在程序运行时,有可能会出现一些错误,导致程序无法正常运行

而有时候我们需要将出现的问题输出,更为具体地通俗易懂地告诉用户出现了什么问题(毕竟报错信息并不是所有人都看得懂的)

这个情况下,我们就要使用try-catch语句

try语句块中,我们放置可能会出现异常的代码,而catch语句块中,我们放置处理异常的代码

另外还有finally语句块,无论是否出现异常,都会执行finally语句块中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 // 基本语法
// 必须写的部分try-catch
try
{
// 放可能出现异常的代码
string a = Console.ReadLine();
int b = int.Parse(a);
Console.WriteLine(b);
}
catch (Exception ex)
{
// 当捕获到异常时执行的代码
Console.WriteLine("Failed");
}
// 可选写的部分finally
finally
{
// 无论是否发生异常都会执行的代码
}

运算符

赋值运算符

赋值运算符,即=,将右边的(表达式)值赋给左边的变量

1
2
3
int a = 10;
int b = 20;
b = b + 2 * 5 / 3 + 5 % 2;

不论=右边是什么,都是先运算右边的,再赋值给左边的变量

算术运算符

就像数学中的加减乘除,在程序中也有对应的

+ 加、- 减、* 乘、/ 除、% 取余(即除不尽的则取它的余数,除得尽的为0)

1
2
3
4
5
6
7
8
9
10
11
int a = 1;
int b = 2;
int c = a + b;

int d = a - b;

int e = a * b;

int f = a / b; // 如果两个整型相除,结果还是整型,且不会四舍五入

int g = a % b;

自增自减运算符

自增运算符,即++,将变量值加1;
自减运算符,即--,将变量值减1

但它们与变量的位置不同,结果也不同

1
2
3
4
5
int a = 1;
int b = a++; // 先赋值,再自增,即先将a的值赋给b,再将a加1
Console.WriteLine(a); // 2
int c = ++a; // 先自增,再赋值,即先将a加1,再将a的值赋给c
Console.WriteLine(a); // 3

复合运算符

它是=和算术运算符的结合,用于简化代码

+= 加等、-= 减等、*= 乘等、/= 除等、%= 取余等

1
2
3
4
5
6
7
8
9
a += b; // 等效 a = a + b;

a -= b; // 等效 a = a - b;

a *= b; // 等效 a = a * b;

a /= b; // 等效 a = a / b;

a %= b; // 等效 a = a % b;

关系运算符

关系运算符,用于比较两个值的大小关系,结果为bool类型

== 等于、!= 不等于、> 大于、< 小于、>= 大于等于、<= 小于等于

1
2
3
4
5
6
7
int h = 3;
Console.WriteLine(a == h);
Console.WriteLine(a != h);
Console.WriteLine(a > h);
Console.WriteLine(a < h);
Console.WriteLine(a >= h);
Console.WriteLine(a <= h);

任意数值可以直接进行比较

但特殊类型stringcharbool只能同类型进行==!=比较

逻辑运算符

逻辑运算符,用于连接多个条件,结果为bool类型

&& 逻辑与(and)、|| 逻辑或(or)、! 逻辑非(not)

1
2
3
Console.WriteLine(a > h && b > h);
Console.WriteLine(a > h || b > h);
Console.WriteLine(!(a > h));

其中,逻辑非!的优先级最高,逻辑与&&的优先级次之,逻辑或||的优先级最低

位运算符

位运算符,用于对二进制位进行操作,结果为int类型

& 按位与、| 按位或、^ 按位异或、~ 按位取反、<< 左移、>> 右移

1
2
3
4
5
6
7
8
9
10
11
12
int i1 = 2;
int j1 = 3;
Console.WriteLine(i1 & j1); // 2
Console.WriteLine(i1 | j1); // 3
Console.WriteLine(i1 ^ j1); // 1

// ~ 取反
Console.WriteLine(~i1); // -3
// << 左移,相当于乘以2的n次方
Console.WriteLine(i1 << 1); // 4
// >> 右移,相当于除以2的n次方
Console.WriteLine(i1 >> 1); // 1

三目运算符

表达式1 ? 表达式2 : 表达式3

如果表达式1为真,则返回表达式2的值;如果表达式1为假,则返回表达式3的值

1
2
3
int k = 1;
int l = 2;
int m = k > l ? k : l; // 2

字符串拼接

字符串拼接可以用算术运算符的+,将两个字符串连接在一起

1
2
string str = "hello";
Console.WriteLine(str + " world");

也可以使用{}占位符,对字符串进行格式化,占位符中放置从0开始的索引,表示要插入的变量

1
2
3
string str1;
str1 = String.Format("{0} world", str); // {}占位符
Console.WriteLine(str1);

括号

括号()用于改变运算符的优先级,括号内的表达式先运算

1
a = (c + d) * e;

运算优先级(只包括上面提到的)

  1. 括号(最高优先级)
    () - 强制改变运算顺序

  2. 自增/自减运算符
    x++ x-- (后缀)

++x --x (前缀)

  1. 算数运算符
    * / % (乘、除、取模)

+ - (加、减)

  1. 位运算符
    << >> (位移)

& (按位与)

^ (按位异或)

| (按位或)

  1. 关系运算符
    < > <= >=

  2. 相等运算符
    == !=

  3. 逻辑运算符
    && (逻辑与)

|| (逻辑或)

  1. 条件运算符(最低优先级)
    ?: (三元运算符)

流程控制

if语句

if语句用于根据条件执行不同的代码块

1
2
3
4
5
语法:
if (条件)
{
条件为真时执行的代码块
}
1
2
3
4
if (true)
{
Console.WriteLine("条件为真");
}

if-else语句

1
2
3
4
5
6
7
8
9
语法:
if (条件)
{
条件为真时执行的代码块
}
else
{
条件为假时执行的代码块
}
1
2
3
4
5
6
7
8
9
int a = 1;
if (a > 5)
{
Console.WriteLine("a大于5");
}
else
{
Console.WriteLine("a小于等于5");
}

if-else if-else语句

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:
if (条件1)
{
条件1为真时执行的代码块
}
else if (条件2)
{
条件2为真时执行的代码块
}
else
{
条件1和条件2都为假时执行的代码块
}
1
2
3
4
5
6
7
8
9
10
11
12
if (a > 5)
{
Console.WriteLine("a大于5");
}
else if (a == 5)
{
Console.WriteLine("a等于5");
}
else
{
Console.WriteLine("a小于5");
}

switch语句

switch类似于简化的if-else if-else语句,用于根据不同的条件执行不同的代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:
switch (表达式)
{
case 常量值1:
执行的代码块1
break;
case 常量值2:
执行的代码块2
break;
default:
执行的代码块3
break;
}

case判断的是表达式所得的值是否与所写的常量值一致,后面只能写常量,而不能写表达式或者变量

同样的,C#switch语句需要写break,不然会贯穿

即执行完一个case后,会继续执行下一个case,直到遇到break或者switch语句结束

1
2
3
4
5
6
7
8
9
10
11
12
13
a = 100;
switch (a)
{
case 1:
Console.WriteLine("a等于1");
break;
case 2:
Console.WriteLine("a等于2");
break;
default: // default可以省略
Console.WriteLine("a不等于1也不等于2");
break;
}

while循环

while循环用于在条件为真时重复执行代码块

但要注意,如果条件永远为真,那么将导致死循环,直至内存溢出,程序崩溃

1
2
3
4
5
语法:
while (条件)
{
执行的代码块
}
1
2
3
4
5
6
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
i++;
}

break

break用于跳出循环,即当满足某个条件时,跳出循环,不再执行循环内的代码

1
2
3
4
5
6
7
8
9
while (true)
{
Console.WriteLine("请输入一个数字:");
string input = Console.ReadLine();
if (input == "1")
{
break;
}
}

continue

continue用于跳过本次循环,即当满足某个条件时,跳过本次循环,继续下一次循环

1
2
3
4
5
6
7
8
9
10
11
int a = 0;
while (a <= 20)
{
if (a % 2 == 0)
{
a++;
continue;
}
Console.WriteLine(a);
a++;
}

do-while循环

do-while循环与while循环类似,但它在循环体执行完毕后,再判断条件是否为真,即无论如何都会执行一次循环体

1
2
3
4
5
语法:
do
{
执行的代码块
} while (条件);
1
2
3
4
5
6
int b = 0;
do
{
Console.WriteLine(b);
b++;
}while (b <= 10);

for循环

for循环用于在指定次数内重复执行代码块

1
2
3
4
5
语法:
for (初始化表达式; 条件表达式; 更新表达式)
{
执行的代码块
}
1
2
3
4
for (int c = 0; c < 10; c++)
{
Console.WriteLine(c);
}

实战小项目#1

需求

控制台显示一个小方块,用户输入wsad,控制小方块上下左右移动

实现

控制台

首先我们设置一下控制台的相关内容

1
2
3
4
5
6
7
8
9
// 设置背景颜色
Console.BackgroundColor = ConsoleColor.Red;
Console.Clear();

// 设置文字颜色
Console.ForegroundColor = ConsoleColor.Yellow;

// 隐藏光标
Console.CursorVisible = false;

绘制小方块

接下来我们绘制小方块

1
2
3
4
5
int x = 0;
int y = 0;

Console.SetCursorPosition(x, y);
Console.Write("■");

不过,对于一个游戏来说,肯定是有循环层的,不然玩家输入一次那就没了可不行

所以,我们要将绘制小方块的代码放入循环语句中,一般我们就采用while (true)循环

1
2
3
4
5
6
7
8
int x = 0;
int y = 0;

while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");
}

获取用户输入

接下来,在控制小方块移动之前,我们得先获取用户输入

1
char c = Console.ReadKey(true).KeyChar;

这里我们使用ReadKey(true)不显示输入的字符,同时设置一个char c变量来接收用户输入的字符

移动小方块

接下来,我们根据用户输入的字符,来移动小方块

那么我们输入的字符只有WASDwasd8种,所以我们可以使用switch语句来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (c)
{
case 'w':
case 'W':
break;
case 's':
case 'S':
break;
case 'a':
case 'A':
break;
case 'd':
case 'D':
break;
}

这里我们就直接利用switch贯穿的特性,即同一个字符的大小写都由同一块代码块来执行

随后,我们在各个case中,根据用户输入的字符,来改变小方块的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
switch (c)
{
case 'w':
case 'W':
y -= 1;
break;
case 's':
case 'S':
y += 1;
break;
case 'a':
case 'A':
x -= 2;
break;
case 'd':
case 'D':
x += 2;
break;
}

好,现在我们输入WASD,小方块会根据输入的字符来移动,但是小方块不会消失,所以我们需要在每次移动之前,将小方块原来的位置清空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");

char c = Console.ReadKey(true).KeyChar;

// 把之前的方块擦除
Console.SetCursorPosition(x, y);
Console.Write(" ");
switch (c)
{
...
}
}

擦除的话就直接写入空格即可,就可以将原来的位置的字符替换掉

限制移动范围

还没结束嘞!

当我们在在边缘位置移动小方块时,假设小方块此时贴在右边界,而我们没注意,继续按D(d),那么小方块就会超出控制台的范围,导致程序崩溃

所以,在各个case语句中,我们还得判断下一个坐标是否越界

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
switch (c)
{
case 'w':
case 'W':
y -= 1;
if (y < 0)
{
y = 0;
}
break;
case 's':
case 'S':
y += 1;
if (y > Console.WindowHeight - 1)
{
y = Console.WindowHeight - 1;
}
break;
case 'a':
case 'A':
x -= 1;
if (x < 0)
{
x = 0;
}
break;
case 'd':
case 'D':
x += 1;
if (x > Console.WindowWidth - 1)
{
x = Console.WindowWidth - 1;
}
break;
}