C# - 表达式与语句

时间:2023-03-08 18:30:16

表达式与语句(Expression&Statement)

操作数(Operands)

1.数字、2.字符、3.变量、4.类型、5.对象、6.方法

操作符(Operator)

参考:C# - 操作符

表达式(Expression)

表达式就是求值的语法式子。表达式至少有1个操作数并且能求出值。

public class Animal { } //不是表达式,因为只有操作数Animal,没有求值
show ( ); //不是表达式,因为只有操作数show,没有求值

语句(Statement)

C#默认代码是以分号结尾,分号前的被视为一条语句。所以一条语句可以跨多行,直到出现分号才结束。多条语句也可置于一行,但必须有分号收尾,没有分号收尾的被视为语句未结束从而抛出异常。

string name = @"xxxxxx
                        xxxxx"; // 分号前的是一条语句

流程控制语句

1.嵌入式语句

嵌入式即这种语句可以无限嵌套N层。所有嵌入式语句只需要键入首个关键单词,按Tab键两次就会自动形成该语句的结构。

①条件语句:if(x)

如果x表达式返回真就会执行if的代码块。

if变体

if(x)、if(z)、……

if可嵌套多个if,如果x表达式返回真就会执行if代码块所包含的其它if代码块。

if(x)、else

如果x表达式返回真就会执行if的代码块,否则执行else代码块。

if(x) 、else if(z)、…… 、else

表示可以有多个else if,如果x表达式返回真就会执行if的代码块,否则判断else if代码块,如果为真就执行它,否则判断下一个else if块……,以此类推,前面所有的判断都为假时,就执行else代码块。

int c = ;
if (c==)
{
    print(c); //属于if语句块
    print(c + ); //属于if语句块
} if (c == ) print(c); //属于if语句块,这种写法只能跟一个表达式,它属于if语句块,多出一个表达式则不再属于if语句块
print(c + );//属于正常流程的表达式,不属于if语句块

②选择语句:switch(x)、case y、break、……、default、break

x是变量,选择一个变量进行判断,y是值类型的操作数,符合case后的值就执行,否则继续选择下一个case,如果所有case都没命中,则执行default,default可以写在开始位置也可以写在最后。case以break收尾,最后default是默认选择也必须以break收尾。 Switch不能选择浮点数,在函数中使用Switch可以使用return替代break,使用return将会同时终止Switch和函数的执行。

switch (变量名)
{
    case 值:
        //逻辑代码……
    break;
    case 值:
        //逻辑代码……
    break;
    //……
    default:
       //逻辑代码……
    break;
}

③抛出异常:throw

此语句支持手动抛出异常信息

int x = 3;
if ( (x | 1) == x)
{
    throw new Exception("x必须是偶数");
}

所有异常类都派生自Exception这个异常基类,下面是可用的一部分异常类型:

System.Exception 其他用户可处理的异常的基本类 
ArgumentException 方法的参数是非法的
ArgumentNullException 一个空参数传递给方法,该方法不能接受该参数
ArgumentOutOfRangeException 参数值超出范围
ArithmeticException 出现算术上溢或者下溢
ArrayTypeMismatchException 试图在数组中存储错误类型的对象
BadImageFormatException 图形的格式错误
DivideByZeroException 除零异常
DllNotFoundException 找不到引用的DLL
FormatException 参数格式错误
IndexOutOfRangeException 数组索引超出范围
InvalidCastException 使用无效的类
InvalidOperationException 方法的调用时间错误
MethodAccessException 试图访问思友或者受保护的方法
MissingMemberException 访问一个无效版本的DLL
NotFiniteNumberException 对象不是一个有效的成员
NotSupportedException 调用的方法在类中没有实现
NullReferenceException 试图使用一个未分配的引用
OutOfMemoryException 内存空间不够
PlatformNotSupportedException 平台不支持某个特定属性时抛出该错误
*Exception 堆栈溢出
SystemException:运行时产生的所有错误的基类
IndexOutOfRangeException:当一个数组的下标超出范围时运行时引发
NullReferenceException:当一个空对象被引用时运行时引发
InvalidOperationException:当对方法的调用对对象的当前状态无效时,由某些方法引发
ArgumentException:所有参数异常的基类
ArgumentNullException:在参数为空(不允许)的情况下,由方法引发
ArgumentOutOfRangeException:当参数不在一个给定范围之内时,由方法引发
InteropException:目标在或发生在CLR外面环境中的异常的基类
ComException:包含COM类的HRESULT信息的异常
SEHException:封装Win32结构异常处理信息的异常
SqlException:封装了SQL操作异常

可替代return

static int Test ( )
{
    throw new Exception ("return没了" );
}

④捕捉异常:try catch

可以使用异常捕捉语句来手动捕获异常并尝试对异常做出各种处理,这样就拦截了运行时可能发生的错误,把处理异常的权限交给了自己。这样做通常是因为有不可预期的错误可能将会发生,为了防患于未然,可以考虑使用该语句。try块用于执行将被监视的代码块,catch用于当try块里的代码发生异常时将其捕获,捕获后你可以自己选择如何做出处理而不是让运行时错误抛出异常。catch可以接收参数或无参数,参数是一个Exception异常实例,不管发生的异常是什么类型统统都派生自Exception这个异常基类,所以你可以写多个catch块测试发生的异常究竟是哪个异常类型,也可以只写一个范围最广的Exception,假如写catch时写了多个异常类型,那么会按照编写的catch的次序依次执行,没命中的异常类型则程序不会进入对应的catch,直到命中匹配的异常后,后面的catch块就不再执行。捕获异常后接下来就是对异常的处理,如:

static void Main( string [ ] args )
{
    string s = Console.ReadLine ( );
    try
    {
        int x = int.Parse ( s );
    }
    catch(Exception e)
    {
        throw; //重新将已经捕获的异常引发,因为这是第二次出现的异常,所以不会被catch到而是直接被抛出。控制台会显示异常信息,但这不是手动捕获而是运行时导致异常被抛出。如果是在web程序中使用空的throw,则异常将对客户端可见。
        throw new Exception ( "这个数据不正确,无法转换" ); //Exception是所有异常的基类,所以你完全可以在捕获异常后手动抛出一个自定义的异常信息。
        Console.WriteLine ( e.Message );//将异常的原始信息输出
        Console.WriteLine ( "除了点小问题,不必着急,没什么大不了" ); //如果根本不希望将异常抛出,比如说为了让用户看到友好的错误提示,就直接输出一段文本即可
    }
    finally
    {
        Console.WriteLine ( "hello" ); //无论如何都要执行的代码块,但如果抛出一个空的throw,finally就没法执行,因为空throw会再次引发该异常却未被明确捕获
    }
}
static void Main( string [ ] args )
{
    string s = Console.ReadLine ( );
    try
    {
        int x = int.Parse ( s );
    }
    catch
    {
        //无参的catch,捕获异常后对其无视,就像他不存在那样……
    }
    finally
    {
        Console.WriteLine ( "hello" ); 
    }
}

附:应用程序的异常事件

每个应用程序都提供这么一个事件,它表示在应用程序发生未被手动捕获的异常时将触发此事件。应将订阅UnhandledException事件的代码置于任何可能抛出异常的代码之前,当异常抛出时,事件会触发,可以做一些异常记录等操作。

string numstr = Console.ReadLine();
           
AppDomain.CurrentDomain.UnhandledException += (objectSender, eventArgs) => 
{
    Trace.Listeners.Add(new TextWriterTraceListener(@"C:\logTest.txt"));
    Trace.AutoFlush = true;
    Trace.WriteLine($"用户输入了错误的字符");
};
int intnum = int.Parse(numstr);

⑤循环:while(x)

x是布尔值。while的块会重复执行,每次执行前会判断x的真假,真就执行。

int i = 1;
bool b = true;
while (b)
{
    b = (i < 100) ? true : false;
    if (i % 2 == 0)
    {
        Console.WriteLine(i);
    }
    i++;
}

whlie变体

do while(x)

x是布尔值。无论x是否是真,至少执行1次do块里的代码逻辑。如果x是真就一直执行。

int i = 1;
do
{
    Console.Write(i);
    i++;
    if (i >= 100)
    {
        break;
    }
} while (i > 0);

⑥for计数循环:for(x;y;z)

for专用于需要计数的循环,比while更简洁也容易控制一个在块内声明的变量。当x满足y时会马上进入循环而不是马上执行z处的计数,在循环1次后,计数器才会进行累加/累减,计数完成后再执行y处的判断,如果是真就继续执行循环。

//9-1=8
for (int i = 1; i < 9; i++)
{
    Console.WriteLine(i);
}

//9-1+1=9
for (int i = 1; i <= 9; i++)
{
    Console.WriteLine(i);
}

//9-0=9
for (int i = 9; i > 0; i--)
{
    Console.WriteLine(i);
}

//9-0+1=10
for (int i = 9; i >= 0; i--)
{
    Console.WriteLine(i);
}

//200+3=203
for (int i=200;i>-3;i--)
{
    Console.WriteLine(i);//end:-2
}

//取最小循环数,9次
for (int x = 1, y = 10; x < 10 && y > 0; x++, y--)
{
    Console.WriteLine($"x={x}");
    Console.WriteLine($"y={y}");
}

for (; ; ) { Console.WriteLine("loop dead") } //死循环
for(int i=1;i<10;i++)
{
    for(int z=1;z<=i;z++)
    {
        Console.Write($"{z} x {i} = {z * i}  ");                    
    }
    Console.Write("\r\n");
}

C# - 表达式与语句

⑦集合迭代:foreach(x in array)

循环枚举集合中的每个元素,array是集合,x是与该集合里的元素的类型相同的变量。

int[] ary = { 1, 87, 35, 56 };
foreach (var i in ary)
{
    Console.WriteLine(i);
}

附:利用Equals方法确认循环是否是首次执行

⑧清理对象:using

可以将一个使用过后需要立即释放的对象放在using语句里 在using代码块中 该对象可以被使用 而一旦离开代码块 该对象就会被自动释放、清理该对象占用的内存资源 比如读取文件的FileStream对象 可以在使用完自动被释放 而无需手动调用close方法来释放,而且using还可以嵌套。

using (FileStream stream = new FileStream("d:/1.txt", FileMode.Open, FileAccess.Read))
{
    //读取文件的逻辑……
}

其实using实际上一个try finally块 它的逻辑如下

FileStream stream = null;
try
{
    stream = new FileStream("d:/1.txt", FileMode.Open, FileAccess.Read);
    //读取文件……
}
finally
{
    if (stream != null)
    {
        ((IDisposable)stream).Dispose();
    }
}

2.声明式语句

声明普通变量

关键字:int、string、struct、object、用户自定义类型……

数据最终存储在内存中,但需要一个标识符来指向内存数据的地址。变量就是用于指向某个在栈上或堆上的数据的地址。变量在被调用前都必须先初始化,未初始化的变量会引发异常。声明变量时应先声明变量的类型,然后定义变量标识符。任何变量都只能在类型、方法中被声明,而在类型中声明的变量被称为字段。声明变量有以下两方式。

// 第一种,先声明再赋值
int a;
a = 
// 第二种,声明同时赋值
int a = ;
// 声明多个变量可用逗号隔开
int a, b, c;
a = ; b = ; c = ; int a = , b = , c = ; // 或每个变量以分号收尾
int a = ;
int b = ;
int c = ;

声明隐式类型的局部变量

关键字:var

方法中的局部变量可使用关键字var声明,这样的类型称为隐式类型,使用var可以不用显示地声明变量的类型,var变量的具体类型是由编译器根据赋给变量的值的类型推断出的,编译器会自动根据值的类型确认变量的类型。

//var的声明只能在方法体内进行,方法参数不能使用var
//匿名变量
var x = 10;
//匿名变量的对象
//匿名对象只能定义只读的字段,没有方法
var y = new { title = "存粹理性批判", author = "康德" };
//可以将匿名对象的字段设为另一个对象的字段、属性
var z = new { new Book().Title, new Book().Author }; //Book对象的Title属性和Author字段将作为匿名对象的字段名和值
//两个具有相同字段但字段出现的顺序不一致的匿名对象不能相互赋值,除非字段同名且字段出现顺序一致
var h = new { Author = "", Title = "寂静的春天" };
var k = new { Title = "寂静的春天", Author = "" };
h = k; //error

声明动态类型的变量

关键字:dynamic

声明动态类型的变量,与var不同,dynamic的变量是在运行时被解析,而var是在编译期解析。所以下面的语法格式都是正确的。

dynamic dynamicObject =new { name="sam" };
Console.WriteLine(dynamicObject.name); //虽然不能点出属性名,但这样写编译器会忽略检查,交由运行时确定此表达式是否正确
//任何类型都能赋值给dynamic类型的变量。
dynamic d1 = ;
dynamic d2 = "a string";
dynamic d3 = System.DateTime.Today;
dynamic d4 = System.Diagnostics.Process.GetProcesses(); 

作用1:当使用linq来投影查询结果,将指定列的数据装入匿名对象后,你可能想要将匿名对象结果集作为参数传递给另一个函数进行处理,此时函数接收的参数不可能是匿名的,因为匿名不具有可声明的类型,此时也可考虑使用dynamic来声明匿名参数。

作用2:动态对象在某种程度上可以代替反射的代码量,比如像下面这样使用动态对象

public class Animal
{
    public string Get()
    {
        return "sam";
    }
} class Program
{
    static void Main(string[] args)
    {
        Type t = typeof(Animal);
        System.Reflection.MethodInfo m = t.GetMethod("Get");
        Animal a = new Animal();
        Console.WriteLine(m.Invoke(a, null));
    }
}
//改为
class Program
{
    static void Main(string[] args)
    {
        dynamic a = new Animal();
        string name = a.Get();
        Console.WriteLine(name);
    }
}

在操纵Xml节点时,也可以利用dynamic替代链式的linq查询,比如Linq查询中会使用大量的链式操作来获取某个xml节点元素,而如果可以将每个节点的名称视为一个对象,则可以直接通过父元素.子元素的方式获取该子元素,这样显得更为优雅简洁。为了实现这个效果,可以自定义一个DynamicXml类,让其从System.Dynamic.DynamicObject类派生,然后针对性的重写对你有价值的虚方法即可。DynamicObject提供10多个虚方法,你可以按需重写这些方法来把linq的代码量化繁为简。

using System.Reflection;
using System.Dynamic;
using System.Xml.Linq; namespace ConsoleApp
{
    public class DynamicXml : DynamicObject
    {
        public XElement Element { get; set; }
        //将参数提供的XElement对象赋给Element属性
        public DynamicXml(XElement e)
        {
            Element = e;
        }
        //根据参数创建XElement对象
        public static DynamicXml Parse(string xmlText)
        {
            return new DynamicXml(XElement.Parse(xmlText)); //Parse方法:根据xml字符创建XElement对象
        }         //重写DynamicObject的两个虚方法TryGetMember和TrySetMember
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = null;
            XElement firstChild = Element.Descendants(binder.Name).FirstOrDefault();
            if (firstChild == null) return false;
            result = firstChild.Descendants().Count() >  ? (object)new DynamicXml(firstChild) : (object)firstChild.Value;
            return true;
        }         public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            XElement firstChild = Element.Descendants(binder.Name).FirstOrDefault();
            if (firstChild == null) return false;
            if (value.GetType() == typeof(XElement)) firstChild.ReplaceWith(value);
            else firstChild.Value = value.ToString();
            return true;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            string xml = @"<Person>
                     <name>sam</name>
                     <gender>man</gender>
                     <age>32</age>
                   </Person>";
            dynamic Person = DynamicXml.Parse(xml);
            Console.WriteLine("{0}\n{1}\n{2}", Person.name, Person.gender, Person.age); //现在可以像获取对象属性那样获取xml元素,非常简洁
            //你还可以随意切换到XElement对象,在其上执行Linq查询
            XElement e = Person.Element;
            XElement xNode = e.Descendants().Where(node => node.Value == "man").FirstOrDefault();
            Console.WriteLine(xNode.Parent.Name);
        }
    }
}

变量的作用域

除了out类型的变量,其它变量只能在其作用域范围内可以被访问,变量一旦离开它的作用域则无法被访问,我们可以这样来理解变量的作用域:块就是一个被{}括起来的区块,这个区块就是变量的作用域。我们总是将变量定义在类型或方法中,而块有父子嵌套的形式,子块定义的变量父块总是无法访问的,而父块定义的变量子块就可以随意访问。因为子块是属于父块的,父块的东西子块可以使用,而子块的东西父块不能拿来用。而且每个子块又是独立的,所以两个子块中定义的变量也不能相互访问。if块else块也是两个独立的块,所以在if块中定义的变量else块也是无法访问的,for循环的计数器变量如果定义在for块中,那么其他块同样无法访问。在同一个作用域中不能定义两个同名的变量,比如父块定义了变量x,则子块不能定义父块中已经定义了的同名变量x,因为子块在父块的作用域内。但两个同级别的块可以定义同名变量,这是因为两个同级别的块始终是两个独立的作用域,所以变量同名并不会发生错误。

3.命令式语句

标签跳转:goto

设定标签,goto可执行该标签。

hello: Console.WriteLine("hello");
goto hello;

终止当前循环:continue

continue是一个强制开关,强制关掉当前循环并强制开启新一次的循环。

结束所有循环:break

break会终止循环的执行,在函数中使用循环时,可以用return代替break,这会终止循环同时终止函数的执行。

终止方法执行:return

return语句放在方法体内,无论return出现在方法中的哪一个子块里,都会影响整个方法,立即终止并返回结果。无论怎样,方法结尾处的代码必须是以return进行收尾。

public void Test(int i)
{
    if (i == 0)
    {
        return;//立即返回,后面的代码就不会执行。
    }
    Console.WriteLine("i!=0");
    return;
}

方法返回值对应的return书写形式有两种,返回void时,return可有可无,返回某种类型时,书写应为:return 某种类型的变量

public string TestStr(string str)
{
    return "test";
} public void Test()
{
    return;
}

写代码有一个“尽早return原则”,看下面的代码:

public string TestStr(string str)
{
    if (!string.IsNullOrEmpty(str))
    {
        //……                
    }
    else
    {
        //……                
    }
    return "";
}

以上代码测试一个字符是否不是空值,不是空值就执行if块里的代码逻辑,假设if块里有成百上千行代码就会影响代码的简洁原则、影响阅读,为此我们应尽量以简化代码量的形式去设计代码,比如尽量不要放太多代码在if块之中,我们可以将上面的语句改为:

public string TestStr(string str)
{
    if (string.IsNullOrEmpty(str))
    {
        return;
    }
    //……
    return "";
}

线程锁:lock

lock语句提供对引用类型的变量的同步访问,它会在指定的变量上附加一个互斥锁直到程序离开互斥锁的代码块变量才会被解锁。使用lock,则一次只会有一个线程能独占互斥锁代码块并访问该变量,而其它线程将处于休眠状态,它们会队列等待访问该变量。 参看:C# - 多线程(基础)

迭代、枚举、循环、遍历、递归的区别

迭代总是利用循环对集合列表的每一个元素进行访问

枚举总是利用循环对集合列表中的常量进行访问

遍历总是利用递归去实现对树状结构的每一个数据节点进行访问

C# - 学习总目录