1. 准备开发环境
1.1 Visual Studio
C#是微软的亲儿子,那么开发工具当然首推宇宙第一好用也是宇宙第一复杂的究极变态牛逼的Visual Studio进行开发。
首先下载Visual Studio并安装,目前版本推荐Visual Studio 2019,不过2022版本即将出预览版随之就是正式版,总之就是推荐最新的。最新版本性能更优,功能更全更方便且bug更少,且据说Visual Studio 2022将是64位版本,使用起来将会更加流畅,不会因为工程较大,因为内存问题卡顿了。
下载地址:Visual Studio IDE、代码编辑器、Azure DevOps 和 App Center - Visual Studio (microsoft.com)
安装的话,把和.Net相关的组件勾选就好了,不需要的最好就不要选了,挺占空间的。
下面示例创建一个Hello World工程:
选择创建控制台应用程序:
然后运行就好了。
1.2 Visual Studio Code
Visual Studio Code也是很好用的C#开发工具,做的也特别好,这个基本上啥都能开发,安装对应的插件就好了,具体优缺点就不细说了,这俩你觉得哪个顺眼用哪个就好了,下面介绍一下如何使用Visual Studio Code开发C#。
下载安装:Visual Studio Code - Code Editing. Redefined
安装C#插件
创建一个文件夹并使用vscode打开此文件夹,点击"Terminal",输入命令"dotnet new console"创建一个新的控制台应用。
创建成功后打开Program.cs,当第一次打开时,如果在安装C#扩展时没有没有下载并安装c#依赖项。比如OmniSharp、Razor语言服务器和.net core调试器等。
在终端输入"dotnet run",将会运行程序。
2. C#类型
C# 是一种强类型语言。 每个变量和常量都有一个类型,每个求值的表达式也是如此。 每个方法声明都为每个输入参数和返回值指定名称、参数数量以及类型和种类(值、引用或输出)。 .NET 类库定义了一组内置数值类型以及表示各种逻辑构造的更复杂类型(如文件系统、网络连接、对象的集合和数组以及日期)。 典型的 C# 程序使用类库中的类型,以及对程序问题域的专属概念进行建模的用户定义类型。
类型中可存储的信息包括以下项:
- 类型变量所需的存储空间。
- 可以表示的最大值和最小值。
- 包含的成员(方法、字段、事件等)。
- 继承自的基类型。
- 它实现的接口。
- 在运行时分配变量内存的位置。
- 允许执行的运算种类。
2.1 常用类型
下表列出了 C# 内置值类型:
下表列出了 C# 内置引用类型:
C# 类型关键字 | .NET 类型 |
---|---|
object | System.Object |
string | System.String |
dynamic | System.Object |
先把内置类型罗列在这里,和其他流行语言差别不大。
2.2 变量和命名
在使用变量时,首先应该考虑它在内存中占多大空间,然后考虑它的处理速度有多快。每一个变量都有一个具体的类型。对于事物标题命名最好遵循以下约定:
命名约定 | Examples | 使用场景 |
---|---|---|
驼峰样式 | cost, orderDetail, dateOfBirth | 局部变量、私有变量 |
标题样式 | String, Int32, Cost, DateOfBirth, Run | 类型、非私有字段以及其他成员(如方法) |
且用于命名的单词较为简单准确表达变量所代表的的事物或方法。
使用示例:
static void Main(string[] args)
{
int a = 1;
double b = 1.1; //双精度浮点数
float c = 2.2F; //单精度浮点数,需要通过F表明其为float类型,否则默认为double类型,无法隐式转换为float类型
decimal d = 0.1M;
Console.WriteLine($"size of int: {sizeof(int)}");
Console.WriteLine($"size of double: {sizeof(double)}");
Console.WriteLine($"size of float: {sizeof(float)}");
Console.WriteLine($"size of decimal: {sizeof(decimal)}");
Console.WriteLine($"a: {a}");
Console.WriteLine($"b: {b}");
Console.WriteLine($"c: {c}");
Console.WriteLine($"d: {d}");
}
运行结果:
size of int: 4
size of double: 8
size of float: 4
size of decimal: 16
a: 1
b: 1.1
c: 2.2
d: 0.1
2.3 比较double类型和decimal类型
static void Main(string[] args)
{
double a = 0.1;
double b = 0.1;
double c = 0.2;
if (a == b) //注意:就算if下只有一个语句,对于语法来说是可以不需要花括号的,但是最好还是加上花括号
{
Console.WriteLine($"{a} equals {b}");
}
else
{
Console.WriteLine($"{a} does not equal {b}");
}
if (a + c == 0.3)
{
Console.WriteLine($"{a} + {c} equals 0.3");
}
else
{
Console.WriteLine($"{a} + {c} does not equal 0.3");
}
}
运行结果:
0.1 equals 0.1
0.1 + 0.2 does not equal 0.3
可以看到a等于b,但是0.1 + 0.2居然不等于0.3。因为double类型并不总是完全精确的。
所以,请记住:永远不要使用==来比较两个double类型的值,否则容易写出一些比较confud的bug哟。
那么如何判断两个double类型的变量是否完全相等呢?
public static bool DoubleEquals(double value1, double value2)
{
//双精度误差
var DOUBLE_DELTA = 1E-6;
return value1 == value2 || Math.Abs(value1 - value2) < DOUBLE_DELTA;
}
static void Main(string[] args)
{
double a = 0.1;
double b = 0.1;
double c = 0.2;
if (DoubleEquals(a, b))
{
Console.WriteLine($"{a} equals {b}");
}
else
{
Console.WriteLine($"{a} does not equal {b}");
}
if (DoubleEquals(a + c, 0.3))
{
Console.WriteLine($"{a} + {c} equals 0.3");
}
else
{
Console.WriteLine($"{a} + {c} does not equal 0.3");
}
}
0.1 equals 0.1
0.1 + 0.2 equals 0.3
下面我们使用decimal类型
static void Main(string[] args)
{
decimal a = 0.1M;
decimal c = 0.2M;
if (a + c == 0.3M)
{
Console.WriteLine($"{a} + {c} equals 0.3");
}
else
{
Console.WriteLine($"{a} + {c} does not equal 0.3");
}
}
运行结果:
0.1 + 0.2 equals 0.3
所以看来decimal类型是精确的。那么它是如何保证精度准确的呢?decimal其实是将数字存储为大的证书并移动小数点。
2.4 关键字var
var类似于c++中的auto关键字,用于类型推断,使用起来非常方便。但是我们一般仅在类型明显时才使用var关键字。
下面仅展示一个使用示例,不做详细阐述。
static void Main(string[] args)
{
decimal a = 0.1M;
var c = a + 0.1M;
if (a + c == 0.3M)
{
Console.WriteLine($"{a} + {c} equals 0.3");
}
else
{
Console.WriteLine($"{a} + {c} does not equal 0.3");
}
}
2.5 内置类型的默认值
除string之外,大多数基本类型都是值类型,也就是说它必须有值。因此可以通过**default()**来获取类型的默认值。
static void Main(string[] args)
{
Console.WriteLine($"default(int) = {default(int)}");
Console.WriteLine($"default(bool) = {default(bool)}");
Console.WriteLine($"default(DateTime) = {default(DateTime)}");
Console.WriteLine($"default(double) = {default(double)}");
Console.WriteLine($"default(string) = {default(string)}");
}
运行结果:
default(int) = 0
default(bool) = False
default(DateTime) = 0001/1/1 0:00:00
default(double) = 0
default(string) =
2.6 数组
static void Main(string[] args)
{
int[] array = new int[3];
array[0] = 1;
array[1] = 2;
array[2] = 3;
foreach (var a in array)
{
Console.WriteLine(a);
}
}
运行结果:
1
2
3
2.7 值类型和引用类型
值类型派生自System.ValueType(派生自 System.Object)。 派生自 System.ValueType 的类型在 CLR 中具有特殊行为。 值类型变量直接包含它们的值,这意味着在声明变量的任何上下文中内联分配内存。 对于值类型变量,没有单独的堆分配或垃圾回收开销。
内置的数值类型是结构,它们具有可访问的字段和方法:
// constant field on type byte.
byte b = byte.MaxValue;
但可将这些类型视为简单的非聚合类型,为其声明并赋值:
byte num = 0xA;
int i = 5;
char c = 'Z';
值类型已密封,这意味着不能从任何值类型(例如 System.Int32)派生类型。 不能将结构定义为从任何用户定义的类或结构继承,因为结构只能从 System.ValueType 继承。 但是,一个结构可以实现一个或多个接口。 可将结构类型强制转换为它实现的任何接口类型;强制转换会导致装箱操作发生,以将结构包装在托管堆上的引用类型对象内。 当你将值类型传递给使用 System.Object 或任何接口类型作为输入参数的方法时,就会发生装箱操作。
使用 struct 关键字可以创建你自己的自定义值类型。 结构通常用作一小组相关变量的容器,如以下示例所示:
public struct Coords
{
public int x, y;
public Coords(int p1, int p2)
{
x = p1;
y = p2;
}
}
另一种值类型是枚举。 枚举定义的是一组已命名的整型常量。 例如,.NET 类库中的 System.IO.FileMode 枚举包含一组已命名的常量整数,用于指定打开文件应采用的方式。 下面的示例展示了具体定义:
public enum FileMode
{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}
System.IO.FileMode.Create
常量的值为 2。 不过,名称对于阅读源代码的人来说更有意义,因此,最好使用枚举,而不是常量数字文本。 有关详细信息,请参阅 System.IO.FileMode。
所有枚举从 System.Enum(继承自 System.ValueType)继承。 适用于结构的所有规则也适用于枚举。 有关枚举的详细信息,请参阅枚举类型。
引用类型
定义为类、记录、委托、数组或接口的类型是引用类型。 在运行时,当声明引用类型的变量时,该变量会一直包含值 null,直至使用 new 运算符显式创建对象,或者为该变量分配已经在其他位置使用 new
创建的对象,如下所示:
MyClass mc = new MyClass();
MyClass mc2 = mc;
接口必须与实现它的类对象一起初始化。 如果 MyClass
实现 IMyInterface
,则按以下示例所示创建 IMyInterface
的实例:
IMyInterface iface = new MyClass();
创建对象后,内存会在托管堆上进行分配,并且变量只保留对对象位置的引用。 对于托管堆上的类型,在分配内存和 CLR 自动内存管理功能(称为“垃圾回收”)回收内存时都会产生开销。 但是,垃圾回收已是高度优化,并且在大多数情况下,不会产生性能问题。
所有数组都是引用类型,即使元素是值类型,也不例外。 虽然数组隐式派生自 System.Array 类,但可以使用 C# 提供的简化语法声明和使用数组,如以下示例所示:
// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };
// Access an instance property of System.Array.
int len = nums.Length;
引用类型完全支持继承。 创建类时,可以从其他任何未定义为密封的接口或类继承,而其他类可以从你的类继承并重写虚拟方法。
2.8 可为空类型
通过问号?来指示类型可为空。
3 类型转换
我们通常需要在不同类型之间转换变量的值。比如说数据在控制台中通常是以文本形式输入,因此我们需要将它转换成为数字或者时间等其他数据类型。还有在网络传输中,服务器接收到的一般都是字符串,所以我们需要转换成我们需要的类型。
- 隐式转换:由于这种转换始终会成功且不会导致数据丢失,因此无需使用任何特殊语法。 示例包括从较小整数类型到较大整数类型的转换以及从派生类到基类的转换。
- 显式转换(强制转换) :必须使用强制转换表达式,才能执行显式转换。 在转换中可能丢失信息时或在出于其他原因转换可能不成功时,必须进行强制转换。 典型的示例包括从数值到精度较低或范围较小的类型的转换和从基类实例到派生类的转换。
- 用户定义的转换:用户定义的转换是使用特殊方法执行,这些方法可定义为在没有基类和派生类关系的自定义类型之间启用显式转换和隐式转换。
- 使用帮助程序类进行转换:若要在非兼容类型(如整数和 System.DateTime 对象,或十六进制字符串和字节数组)之间转换,可使用 System.BitConverter 类、System.Convert 类和内置数值类型的
Parse
方法(如 Int32.Parse)。
3.1 对数字进行转换
将int隐式转换为double是安全的,因为并不会丢失任何信息。然而尝试将double隐式转换为int时,编译器将会报错,因为可能会丢失信息。
我们可以通过强制类型转换将double类型转换为int类型,此时小数部分将自动删除。
此外我们还可通过System.Convert进行强制类型转换。
using System;
using static System.Convert;
namespace vscode
{
class Program
{
static void Main(string[] args)
{
double a = 9.9;
int b = ToInt32(a);
Console.WriteLine($"b: {b}");
}
}
}
运行结果:
b: 10
可以看到并不是去掉小数点后的部分,而是看起来像是四舍五入。
其实System.Convert的取整方式并不是完全的四舍五入,而是:小数部分大于0.5向上取整,小于0.5向下取整,等于0.5则在非小数部分是奇数才向上取整,偶数则向下取整。
static void Main(string[] args)
{
double a = 9.5;
int b = ToInt32(a);
double c = 10.5;
int d = ToInt32(c);
Console.WriteLine($"b: {b}");
Console.WriteLine($"d: {d}");
}
运行结果:
b: 10
d: 10
而真正的四舍五入可以通过Math.Round来实现。
3.2 Parse和TryParse
10
可以看到并不是去掉小数点后的部分,而是看起来像是四舍五入。
其实System.Convert的取整方式并不是完全的四舍五入,而是:小数部分大于0.5向上取整,小于0.5向下取整,等于0.5则在非小数部分是奇数才向上取整,偶数则向下取整。
```c#
static void Main(string[] args)
{
double a = 9.5;
int b = ToInt32(a);
double c = 10.5;
int d = ToInt32(c);
Console.WriteLine($"b: {b}");
Console.WriteLine($"d: {d}");
}
运行结果:
b: 10
d: 10
而真正的四舍五入可以通过Math.Round来实现。
3.2 Parse和TryParse
我们可以通过Parse来进行类型转换,但是它的缺点是失败的话会报异常,此时则可通过TryParse避免程序崩溃。