C# 基础、核心概念和模式交互式指南(四)
十三、异常处理
关于异常处理的讨论
老师开始讨论:一般来说,当我们为一个应用编写代码时,我们期望它总是能够顺利执行。但是有时候,我们在执行那些程序的时候会遇到突然的惊喜。这些意外可能以各种方式出现,并通过一些粗心的错误(例如,试图实现错误的逻辑,或忽略程序代码路径中的一些漏洞等)出现。)然而,许多失败都超出了程序员的控制范围,这也是事实。我们经常把这些不想要的情况称为例外。当我们编写应用时,处理这些异常是必不可少的。
定义
我们可以将异常定义为一个事件,它打破了正常的执行/指令流。
当出现异常情况时,会创建一个异常对象并将其抛出到创建该异常的方法中。该方法可能会也可能不会处理异常。如果它不能处理异常,它将把责任传递给另一个方法。(类似于我们的日常生活,当情况超出我们的控制范围时,我们会向他人寻求建议)。如果没有负责处理特定异常的方法,则会出现一个错误对话框(指示未处理的异常),并且程序的执行会停止。
Points to Remember
异常处理机制处理。如果处理不当,应用会过早死亡。因此,我们应该尝试编写能够以优雅的方式检测和处理意外情况的应用,并防止应用过早死亡。
让我们从一个简单的例子开始。下面的程序将成功编译,但它将在运行时引发一个异常,因为我们忽略了除数(b)是 0 的事实(即,我们将 100 除以 0)。
演示 1
using System;
namespace ExceptionEx1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Exceptions.***");
int a=100, b=0;
int c = a / b;
Console.WriteLine(" So, the result of a/b is :{0}", c);
Console.ReadKey();
}
}
}
输出
系统。DivideByZeroException:“试图除以零。”
老师继续说:在继续之前,我将强调一些关于异常处理机制的要点。你必须反复检查这些要点。
- 中的所有例外。NET 是对象。
- 系统。Exception 是异常的基类。
- 应用中的任何方法都可能在应用运行时引发意外。如果出现这种情况,在编程术语中,我们说该方法抛出了一个异常。
- 我们使用以下关键字来处理 C# 异常:try、catch、throw、finally
- 我们试图用 try/catch 块来保护异常。可能引发异常的代码放在 try 块中,这种异常情况在 catch 块中处理。
- 我们可以将多个 catch 块与一个 try 块相关联。当一个特定的 catch 块处理突发事件时,我们说 catch 块已经捕获了异常。
- finally 块中的代码必须执行。finally 块通常放在 try 块或 try/catch 块之后。
- 当 try 块中引发异常时,控件将跳转到相应的 catch 或 finally 块。try 块的剩余部分将不会被执行。
- 异常遵循继承层次结构。有时,如果我们将可以处理父类异常的 catch 块(例如,catch block1)放在只能处理派生类异常的 catch 块(例如,catch block2)之前,我们可能会遇到编译时错误。从编译器的角度来看,这是一个不可达代码的例子,因为在这种情况下,catch block1 已经能够处理 catch block2 可以处理的异常。因此,控制根本不需要到达 catch block2。我们将通过一个例子来研究这种情况。
- 我们可以使用任何组合:try/catch、try/catch/finally 或 try/finally。
- finally 块中的代码必须执行。
- 如果我们不处理异常,CLR 将代表我们捕获它,我们的程序可能会过早死亡。
与 Java 有一个关键区别:这里所有的异常都是隐式未检查的。因此,C# 中没有 throws 关键字的概念。这是一个争论的热门话题。
老师继续说:现在让我们看看如何处理我们在前面的例子中遇到的异常。
演示 2
using System;
namespace ExceptionEx1Modified
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Exceptions***");
int a = 100, b = 0;
try
{
int c = a / b;
Console.WriteLine(" So, the result of a/b is :{0}", c);
}
catch (Exception ex)
{
Console.WriteLine("Encountered an exception :{0}", ex.Message);
}
finally
{
Console.WriteLine("I am in finally
.You cannot skip me!");
}
Console.ReadKey();
}
}
}
输出
分析
我们可以从程序的输出中确认以下几点:
- 当 try 块中引发异常时,控件跳转到相应的 catch 块。try 块的剩余部分没有执行。
- 尽管我们遇到了异常,finally 块中的代码还是执行了。
- 我们使用了一个名为 Message 的公共属性。在系统中。例外,还有一些众所周知的属性。在 Visual Studio 中可以很容易地看到它们。
如箭头所示,在大多数情况下,您可能需要这三个属性:Message、StackTrace 和 InnerException。本章在各种示例中使用了 Message 和 StackTrace 属性。从截图中可以很容易地看出,这些都是只读属性(它们只有 get 属性)。为了便于您立即参考,我展开了这三个属性以显示它们的描述。
- InnerException 属性:获取系统。导致当前异常的异常实例。
- Message 属性:描述当前异常。
- StackTrace 属性:使用该属性,我们可以获得导致异常的方法调用的层次结构。它为我们提供了调用堆栈上直接帧的字符串表示。
学生问:
先生,我们可以很容易地在除法运算之前放置一个 if 块,如 if(b==0 ),以避免除数为 0,在这种情况下,我们可以很容易地排除使用 try/catch 块。
老师澄清道:“你只是在考虑这个简单的例子,这就是为什么它会以这种方式出现在你面前。是的,在这种情况下,你的除数是固定的,你可以用那种方式保护你的代码。然而,考虑这样一种情况,b 的值也是在运行时计算的,并且您不能提前预测该值。此外,如果在所有可能的情况下都需要这样的保护,你的代码可能看起来很笨拙,很明显不可读。”
老师继续说:为了便于参考,下面是一些在语言规范中定义的异常类。
| 系统。算术异常 | 算术运算期间发生的异常的基类,如 System。DivideByZeroException 和 System.OverflowException。 | | 系统。ArrayTypeMismatchException | 当由于存储元素的实际类型与数组的实际类型不兼容而导致数组存储失败时,将引发此异常。 | | 系统。DivideByZeroException | 当试图将整数值除以零时会引发此异常。 | | 系统。IndexOutOfRangeException | 当试图通过小于零或超出数组边界的索引对数组进行索引时,将引发此异常。 | | 系统。异常 | 当在运行时从基类型或接口到派生类型的显式转换失败时,将引发此异常。 | | 系统。空引用的异常 | 当使用空引用的方式导致需要被引用的对象时,将引发此异常。 | | System.OutOfMemoryException | 当分配内存(通过 new)的尝试失败时抛出。 | | 系统。堆栈溢出异常 | 当由于有太多挂起的方法调用而耗尽执行堆栈时,将引发此异常;通常表示非常深或无限的递归。 | | 系统。TypeInitializationException | 当静态构造函数抛出异常,并且没有 catch 子句来捕获它时,将引发该异常。 | | 系统。溢出异常 | 当检查的上下文中的算术运算溢出时,将引发此异常。 |有关更详细的异常列表,可以在 Visual Studio 中按 Ctrl+Alt+E,然后展开公共语言运行时异常选项,如下面的屏幕截图所示。
现在考虑下面的例子,看看如何在我们的程序中用多个 catch 块处理多个异常。
演示 3
using System;
namespace HandlingMultipleEx
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Handling multiple Exceptions***");
string b1;
int input;
Console.WriteLine("Enter your choice( 0 or 1)");
b1 = Console.ReadLine();
//Checking whether we can parse the string as an integer
if (int.TryParse(b1, out input))
{
Console.WriteLine("You have entered {0}", input);
switch (input)
{
case 0:
int a = 100, b = 0;
try
{
int c = a / b;
Console.WriteLine(" So, the result of a/b is :{0}", c);
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Encountered an exception with integers:{0}", ex.Message);
Console.WriteLine("Encountered an exception with integers:{0}", ex.StackTrace);
}
catch (Exception ex)
{
Console.WriteLine("In Choice0.Exception block ..{0}",ex.Message);
}
break;
case 1:
int[] myArray = { 1, 2, 3 };
try
{
Console.WriteLine(" myArray[0] :{0}", myArray[0]);
Console.WriteLine(" myArray[1] :{0}", myArray[1]);
Console.WriteLine(" myArray[2] :{0}", myArray[2]);
Console.WriteLine(" myArray[3] :{0}", myArray[3]);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("Encountered an exception with array
elements :{0}", ex.Message);
Console.WriteLine("Encountered an exception with array
elements :{0}", ex.StackTrace);
}
catch (Exception ex)
{
Console.WriteLine("In Choice1.Exception block ..{0}", ex.Message);
}
break;
default:
Console.WriteLine("You must enter either 0 or 1");
break;
}
}
else
{
Console.WriteLine("You have not entered an integer!");
}
Console.ReadKey();
}
}
}
输出
案例 1:用户输入了 0。
情况 2:用户输入了 1。
案例 3:用户输入了一个字符串。
分析
我们可以从程序的输出中确认以下几点:
- 当引发异常时,只执行一个 catch 子句。例如,如果 block-catch(DivideByZeroException ex){..}可以处理异常,block- catch (Exception ex){..}不需要进入画面。
- 在前面的程序中,所有类型的异常(除了 DivideByZeroException 和 IndexOutOfRangeException)都在 block- catch (Exception ex)中捕获,并且该块必须作为最后一个 catch 块放置,因为 System。异常类是所有异常的基类。
恶作剧
你能预测产量吗?
演示 4
using System;
namespace Quiz1Exception
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Exceptions***");
int a = 100, b = 0;
try
{
int c = a / b;
Console.WriteLine(" So, the result of a/b is :{0}", c);
}
catch (ArithmeticException ex)
{
Console.WriteLine("Encountered an exception :{0}", ex.Message);
}
//Error:Exceptions follows the inheritance
hierarchy.
//So, we need to place catch blocks properly.
catch (DivideByZeroException ex)
{
Console.WriteLine("Encountered an DivideByZeoException :{0}", ex.Message);
}
Console.ReadKey();
}
}
}
输出
编译器错误。
分析
异常遵循继承层次结构。因此,我们需要适当地放置 catch 块。在这种情况下,DivideByZeroException 是 ArithmeticException 的子类(而 arithmetic Exception 又是 Exception 的子类)。您可以在 Visual Studio 中轻松检查这一点。
Points to Remember
因此,当您处理多个 catch 块时,您需要首先放置更具体的异常子句。
catch 条款的其他变体
老师继续说:到目前为止,我们已经看到了不同的 catch 块。我们将注意到,您可以简单地使用 catch( )或 catch{},而不是 catch( )。因此,下面的代码块不会引发任何编译错误。这是 catch 子句的一种变体。
catch (Exception)
{
Console.WriteLine("Encountered an Exception");
}
这个街区也可以。这是 catch 子句的另一种变体。
catch ()
{
Console.WriteLine("Encountered an Exception");
}
但是,强烈建议您尽量避免这两种 catch 块。
恶作剧
代码会编译吗?
//some code before
catch (Exception)
{
Console.WriteLine("Encountered an Exception");
}
catch { }
//some code after
回答
是的。但是,在 Visual Studio 2017 中,您会看到这条警告消息:
恶作剧
代码会编译吗?
//some code before
catch { }
catch (Exception)
{
Console.WriteLine("Encountered an Exception");
}
//some code after
回答
不会。在 Visual Studio 2017 中,您会看到以下错误信息:
说明
按照语言规范,没有命名异常类的 catch 子句可以处理任何异常。此外,还有一些不是从 System.Exception 派生的异常,称为非 CLS 异常。一些。NET 语言(包括 C++/CLI)支持这些异常。在 Visual C# 中,我们不能抛出非 CLS 异常,但我们可以捕捉它们。默认情况下,Visual C# 程序集将非 CLS 异常捕获为包装异常(请参见上一个测验输出中的警告消息)。因此,我们也可以在 block-catch (Exception ex){..}.
专家建议,当您知道需要执行某些特定任务(例如,写入日志条目)来响应非 CLS 异常,但不需要访问异常信息时,可以使用 catch{}。关于这个话题的更多信息,可以去 https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/exceptions/how-to-catch-a-non-cls-exception 。
老师继续说:我们有 catch 块的另一个变体,它是在 C# 6.0 中引入的。
下面是 catch 子句的第三种变体。
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
//some code
}
在这种情况下,when 子句就像一个过滤器。因此,在这种情况下,如果抛出了 WebException,但是布尔条件(后面跟有 when)不为真,这个 catch 块将不会处理该异常。因此,使用这种过滤器,我们可以再次捕获相同的异常,但在不同的 catch 块中处理它,如下所示:
catch (WebException ex) when (ex.Status == WebExceptionStatus.Pending)
{
//some code
}
或者,
catch (WebException ex) when (ex.Status == WebExceptionStatus.ProtocolError)
{
//some code
}
老师继续说:现在我们来看看一个方法如何抛出异常。方法可以用 throw 关键字抛出异常。在下面的例子中,当除数为 0 时,我们从 Divide 方法中抛出了 DivideByZeroException,然后在 catch 块中处理它。
演示 5
using System;
namespace ThrowingExceptionEx
{
class Program
{
static int a = 100, b = 0, c;
static void Divide(int a, int b)
{
if (b != 0)
{
int c = a / b;
}
else
{
throw new DivideByZeroException("b comes as Zero");
}
}
static void Main(string[] args)
{
Console.WriteLine("***Exploring Exceptions:Throwing an Exception Example***");
try
{
Divide(a, b);
Console.WriteLine("Division operation completed");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Encountered an exception :{0}", ex.Message);
}
Console.ReadKey();
}
}
}
输出
学生问:
先生,有什么不同的方法来提出一个例外?
老师说:一般来说,有两种不同的方式可以提出异常。
方法 1:我们刚刚看到,任何方法都可以通过使用 throw 关键字来引发异常。该语句会立即引发异常,并且控制权不会转移到紧跟在 throw 语句之后的语句。
方法 2:当我们处理 C# 语句和异常时,我们可能会遇到由于错误的逻辑、漏洞等等导致的异常。
老师继续说:有时我们需要反复抛出(称为再抛出)一个异常。在某些情况下是必要的;例如,当我们想要写一个日志条目或者当我们想要发送一个新的更高级别的异常时。
以下是重新引发异常的格式:
try
{
//some code
}
catch(Exception ex)
{
//some code e.g. log it now
//Now rethrow it
throw;
}
Note
如果您使用throw ex而不是throw;,程序将不会有任何编译问题,但是如果您检查 StackTrace 属性,您会发现它与原始属性不同。因此,强烈建议您只有在真正想要重新抛出原始异常时才使用throw;。请参见演示 6 的输出来确认这一点。
演示 6
using System;
namespace RethrowingExceptionEx
{
class Program
{
static int a = 100, b = 1, c;
static void Divide(int a, int b)
{
try
{
b--;
c = a / b;
//some code
}
catch(Exception ex)
{
//some code e.g. log it now
Console.WriteLine("a={0} b={1}", a,b);
Console.WriteLine("Message: {0}", ex.Message);
Console.WriteLine("StackTrace: {0}", ex.StackTrace);
//Now rethrow it
throw; //will throw the current exception
//throw new ArithmeticException();//throwing the parent class exception
}
}
static void Main(string[] args)
{
Console.WriteLine("***Exploring Rethrowing an Exception Example***");
try
{
Divide(a, b);
Console.WriteLine(" Main.Divide() is completed");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("\na={0} b={1}", a, b);
Console.WriteLine("Message: {0}", ex.Message);
Console.WriteLine("StackTrace: {0}", ex.StackTrace);
}
catch (Exception ex)
{
Console.WriteLine("\nIn catch(Exception ex)");
Console.WriteLine("a={0} b={1}", a, b);
Console.WriteLine("Message: {0}", ex.Message);
Console.WriteLine("StackTrace: {0}", ex.StackTrace);
}
Console.ReadKey();
}
}
}
输出
分析
现在您可以明白为什么第一个案例中的日志记录很重要了。一遇到异常,我们就记录下来,然后我们看到除数(b)在 Divide()方法中变成了 0。如果您没有记录它,那么当您看到最终的日志语句时,您可能会想,当 b 为 1 时,为什么会出现这个异常。
取消前面程序中throw new ArithmeticException();行的注释,如下所示:
您将收到以下输出:
学生问:
先生,看来我们可以在这种情况下抛出任何例外。这是正确的吗?
老师说:是的,但是很明显,这是不被推荐的。但是,当您学习创建自己的异常时,您可以将这个原始异常与您的自定义异常消息结合起来,然后重新抛出它以获得更好的可读性。
创建自定义异常
老师继续说:有时我们想定义自己的异常来获得更有意义的信息。在我们继续之前,我们必须记住以下几点:
- 在异常层次结构中,我们注意到两种主要类型的异常类:SystemException 和 ApplicationException。SystemException 由运行时(CLR)抛出,ApplicationException 由用户程序抛出(到目前为止,我们一直使用 System Exception)。最初,有人建议用户定义的异常应该从 ApplicationException 类派生。然而,后来 MSDN 建议:“你应该从 Exception 类而不是 ApplicationException 类派生定制异常。您不应在代码中引发 ApplicationException 异常,也不应捕捉 ApplicationException 异常,除非您打算重新引发原始异常。(见
https://msdn.microsoft.com/en-us/library/system.applicationexception.aspx)。) - 当我们创建自己的异常时,类名应该以单词 exception 结尾。(见
https://docs.microsoft.com/en-us/dotnet/standard/exceptions/how-to-create-user-defined-exceptions)。) - 提供构造函数的三个重载版本(如演示 7 所述)。
当我们创建自己的例外时,我们将尝试遵循所有这些建议。
演示 7
using System;
namespace CustomExceptionEx1
{
class ZeroDivisorException : Exception
{
public ZeroDivisorException() : base("Divisor is zero"){ }
public ZeroDivisorException(string msg) : base(msg){ }
public ZeroDivisorException(string msg, Exception inner) : base(msg, inner)
{ }
}
class TestCustomeException
{
int c;
public int Divide(int a, int b)
{
if (b == 0)
{
//Ex.Message= "Divisor should not be Zero"
throw new ZeroDivisorException("Divisor should not be Zero");
//Ex.Message= "Divisor is Zero"
//throw new ZeroDivisorException();
}
c = a / b;
Console.WriteLine("Division completed");
return c;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A Custom Exception Example***");
int a = 10, b = 1, result;
try
{
b--;
TestCustomeException testOb = new TestCustomeException();
result = testOb.Divide(a, b);
}
catch (ZeroDivisorException ex)
{
Console.WriteLine("Caught the custom exception
: {0}", ex.Message);
}
finally
{
Console.WriteLine("\nExample completed");
Console.ReadKey();
}
}
}
}
输出
分析
我们使用了构造函数的第二个重载版本。如果您想使用默认的构造函数(前面已经注释过了),会有一个不同的消息。
摘要
本章回答了以下问题。
- 什么是例外?
- 我们如何处理程序中的错误?
- 我们在 C# 中处理异常时常用的关键字有哪些?
- 我们应该如何在程序中放置 try、catch 和 block,目的是什么?
- catch 子句有哪些不同的变体?
- 我们如何在程序中使用异常过滤器?
- 我们如何对异常进行分类?
- 我们如何定制例外?
十四、内存清理
教师开始讨论:管理内存是程序员关心的一个重要问题。酪 NET 试图让他们的生活变得更容易,负责清除那些在某个特定点之后没有用处的对象。在编程中,我们称之为脏对象或未引用对象。
垃圾收集器程序作为低优先级线程在后台运行,并跟踪脏对象。。NET 运行库可以定期调用此程序从内存中移除未引用的或脏的对象。
然而,有一个问题。一些对象需要特殊的拆卸代码来释放资源。一个非常常见的例子是当我们打开一个或多个文件,然后执行一些操作(例如,读、写等。)但忘记关闭文件。在其他情况下,也可能需要类似的注意,例如当我们处理程序中的非托管对象、锁定机制或操作系统句柄等时。程序员显然需要释放这些资源。
一般来说,当程序员努力清理(或释放)内存时,我们说他们试图释放对象,但当 CLR 自动处理释放资源时,我们说垃圾收集器正在执行其工作或垃圾收集正在进行。
Points to Remember
程序员可以通过显式释放对象来释放资源,或者 CLR 通过垃圾收集机制自动释放资源。
垃圾收集器如何工作
老师继续说:分代式垃圾收集器用于比长寿命对象更频繁地收集短寿命对象。我们这里有三代:0,1,2。短期对象存储在第 0 代中。生命周期较长的对象被推送到更高的层代—1 或 2。垃圾收集器在低代中比在高代中工作得更频繁。
一旦我们创建了一个对象,它就驻留在第 0 代中。当第 0 代填满时,垃圾收集器被调用。在第一代垃圾收集中幸存下来的对象被提升到下一个更高的代,即第 1 代。在第 1 代垃圾收集中幸存下来的对象进入最高的第 2 代。
Note
您可以记住 3-3 规则:垃圾收集工作在三个不同的阶段,通常,垃圾收集器在三种不同的情况下被调用。
垃圾收集的三个阶段
以下是垃圾收集的三个不同阶段:
- 阶段 1 是标记阶段,在该阶段中,活的物体被标记或识别。
- 阶段 2 是重定位阶段,在此阶段,它更新将在阶段 3 中压缩的对象的引用。
- 阶段 3 是压缩阶段,它从死的(或未被引用的)对象中回收内存,压缩操作在活动的对象上执行。它将活动对象(在此之前一直存在)移动到分段的旧末端。
调用垃圾收集器的三种情况
以下是调用垃圾收集器的三种常见情况:
- 在案例 1 中,我们的内存不足。
- 在情况 2 中,我们分配的对象(在托管堆中)超过了定义的阈值限制。
- 在第三种情况下,系统。调用 GC()方法。
我之前说过 GC。Collect()方法可用于强制垃圾收集机制。这个方法有许多重载版本。在下面的例子中,我们使用 GC。Collect(Int32),强制从第 0 代到指定代立即进行垃圾回收。
为了理解这些概念,让我们检查下面的程序和输出。我们通过调用系统使垃圾收集器开始工作。GC()(案例三)。
演示 1
using System;
namespace GarbageCollectionEx4
{
class MyClass
{
private int myInt;
//private int myInt2;
private double myDouble;
public MyClass()
{
myInt = 25;
//myInt2 = 100;
myDouble = 100.5;
}
public void ShowMe()
{
Console.WriteLine("MyClass.ShowMe()");
}
public void Dispose()
{
GC.SuppressFinalize(this);
Console.WriteLine("Dispose() is called");
Console.WriteLine("Total Memory:" + GC.GetTotalMemory(false));
}
~MyClass()
{
Console.WriteLine("Destructor is Called..");
Console.WriteLine(" After this destruction total Memory:" + GC.GetTotalMemory(false));
//To catch the output at end, we are putting some sleep
System.Threading.Thread.Sleep(60000);
}
}
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("*** Exploring Garbage Collections.***");
try
{
Console.WriteLine("Maximum Generations of GC:" + GC.MaxGeneration);
Console.WriteLine("Total Memory:" + GC.GetTotalMemory(false));
MyClass myOb = new MyClass();
Console.WriteLine("myOb is in Generation : {0}", GC.GetGeneration(myOb));
Console.WriteLine("Now Total Memory is:{0}", GC.GetTotalMemory(false));
Console.WriteLine("Collection occured in 0th Generation:{0}", GC.CollectionCount(0));
Console.WriteLine("Collection occured in 1th Generation:{0}", GC.CollectionCount(1));
Console.WriteLine("Collection occured in 2th Generation:{0}", GC.CollectionCount(2));
//myOb.Dispose();
GC.Collect(0);//will call generation 0
Console.WriteLine("\n After GC.Collect(0)");
Console.WriteLine("Collection occured in 0th Generation:{0}", GC.CollectionCount(0));//1
Console.WriteLine("Collection occured in 1th Generation:{0}", GC.CollectionCount(1));//0
Console.WriteLine("Collection occured in 2th Generation:{0}", GC.CollectionCount(2));//0
Console.WriteLine("myOb is in Generation : {0}", GC.GetGeneration(myOb));
Console.WriteLine("Total Memory:" + GC.GetTotalMemory(false));
GC.Collect(1);//will call generation 1 with 0
Console.WriteLine("\n After GC.Collect(1)");
Console.WriteLine("Collection occured in 0th Generation:{0}", GC.CollectionCount(0));//2
Console.WriteLine("Collection occured in 1th Generation:{0}", GC.CollectionCount(1));//1
Console.WriteLine("Collection ccured in 2th Generation:{0}", GC.CollectionCount(2));//0
Console.WriteLine("myOb is in Generation : {0}", GC.GetGeneration(myOb));
Console.WriteLine("Total Memory:" + GC.GetTotalMemory(false));
GC.Collect(2);//will call generation 2 with 1 and 0
Console.WriteLine("\n After GC.Collect(2)");
Console.WriteLine("Collection occured in 0th Generation:{0}", GC.CollectionCount(0));//3
Console.WriteLine("Collection occured in 1th Generation:{0}", GC.CollectionCount(1));//2
Console.WriteLine("Collection ccured in 2th Generation:{0}", GC.CollectionCount(2));//1
Console.WriteLine("myOb is in Generation : {0}", GC.GetGeneration(myOb));
Console.WriteLine("Total Memory:" + GC.GetTotalMemory(false));
}
catch (Exception ex)
{
Console.WriteLine("Error:" + ex.Message);
}
Console.ReadKey();
}
}
}
输出
分析
再把理论过一遍,理解输出。然后尝试理解垃圾收集是如何发生的。我们可以看到,每当我们调用第 2 代时,其他代也会被调用。
您还可以看到,我们创建的对象最初放置在第 0 代中。
学生问:
先生,我们怎么能调用析构函数呢?
老师说:你不能调用析构函数。垃圾收集器负责这项工作。
学生问:
什么是托管堆?
老师说:当 CLR 初始化垃圾收集器时,它会分配一段内存来存储和管理对象。这种内存称为托管堆。
老师继续:一般来说,Finalize()(或者对象的析构函数)是被调用来清理内存的。因此,我们可以提供析构函数来释放我们的对象所拥有的一些未被引用的资源,在这种情况下,我们需要覆盖 Object 类的 Finalize()方法。
学生问:
垃圾收集器什么时候调用 Finalize()方法?
老师说:我们永远不知道。当发现没有引用的对象时,或者稍后当 CLR 需要回收一些内存时,它可能会立即调用。但是我们可以通过调用 System 来强制垃圾收集器在给定的点运行。GC.Collect(),它有很多重载版本。(我们已经通过调用 GC 看到了一个这样的用法。Collect(Int32))。
学生问:
为什么压缩是必要的?
老师继续说:当 GC 从堆中移除所有预期的对象(即那些没有引用的对象)时,堆中就包含了分散的对象。为了简单起见,你可以假设这是我们的堆。在垃圾收集器的清理操作之后,它可能如下所示(白色块表示空闲/可用块):
你可以看到,如果我们现在需要在我们的堆中分配五个连续的内存块,我们不能分配它们,尽管总的来说我们有足够的空间来容纳它们。为了处理这种情况,垃圾收集器需要应用压缩技术,将所有剩余的对象(活动对象)移动到一端,形成一个连续的内存块。因此,压缩后,它可能看起来像这样:
现在,我们可以轻松地在堆中分配五个连续的内存块。
这样,托管堆不同于旧的非托管堆。(这里我们不需要遍历地址链表来为新数据寻找空间。我们可以简单地使用堆指针;因此,实例化。网速更快)。压缩后,对象通常停留在相同的区域,因此访问它们也变得更容易和更快(因为页面交换更少)。这就是为什么微软也认为,虽然压缩操作的成本很高,但这种效果带来的总体收益更大。
学生问:
我们应该何时调用 GC。Collect()?
老师说:我已经提到过,调用 GC 通常是一个开销很大的操作。但是在一些特殊的场景中,我们绝对相信如果我们能够调用 GC,我们将会获得一些显著的好处。当我们在代码中取消引用大量对象时,可能会出现这样的例子。
另一个常见的例子是,当我们试图通过一些常见的操作来查找内存泄漏时(例如,重复执行测试来查找系统中的泄漏)。在每一次操作之后,我们可能会尝试收集不同的计数器来分析内存增长并获得正确的计数器。我们可能需要打电话给 GC。在每个操作开始时收集()。
我们将很快讨论内存泄漏分析。
学生问:
假设我们需要在应用运行的某个特定时间段回收一定量的内存。我们应该如何着手满足需求?
老师说。NET framework 提供了一个特殊的接口 IDisposable。
我们需要实现这个 IDisposable 接口,作为一个明显的动作,我们需要覆盖它的 Dispose()方法。当开发人员想要释放资源时,这是最佳实践。这种方法的另一个主要优点是,我们知道程序何时会释放未被引用的资源。
Points to Remember
当我们实现 IDisposable 接口时,我们假设程序员会正确地调用 Dispose()方法。一些专家仍然建议,作为预防措施,我们也应该实现一个析构函数。如果没有调用 Dispose(),这种方法可能会很有用。我同意这种双重实现在现实编程中更有意义。
C# 在这种情况下提供了特殊的支持。您可以使用“using语句”来减少代码大小,使其更具可读性。它被用作 try/finally 块的语法快捷方式。
内存泄漏分析
一般来说,当计算机程序运行了很长一段时间,但未能释放不再需要的内存资源时,我们可以感受到内存泄漏的影响(例如,随着时间的推移,机器变得很慢,或者在最糟糕的情况下,机器可能会崩溃)。有了这些信息,很明显“它多快引起我们的注意”取决于我们应用的泄漏率。
考虑一个非常简单的例子。假设我们有一个在线应用,用户需要填写一些数据,然后单击提交按钮。现在假设应用的开发人员错误地忘记了在用户按下提交按钮时释放一些不再需要的内存,由于这种错误判断,应用每次点击会泄漏 512 字节。在一些初始点击中,我们可能不会注意到任何性能下降。但是,如果成千上万的在线用户同时使用该应用,会发生什么呢?如果 10 万个用户点击提交按钮,我们最终将损失 48.8 MB 的内存,10 亿次点击将损失 4.76 GB 的内存,以此类推。
简而言之,即使我们的应用或程序每次执行都会泄漏非常少量的数据,很明显,在一段时间后,我们会看到某种故障;例如,我们可能会注意到我们的设备正在与一个系统崩溃。OutOfMemoryException,或者设备中的操作变得非常慢,以至于我们需要经常重启我们的应用。
在像 C++这样的非托管语言中,当预期的工作完成时,我们需要释放内存;否则,在一段时间内,内存泄漏的影响将是巨大的。在托管代码中,CLR 的垃圾回收器将我们从这些情况中解救出来。尽管如此,仍有一些情况需要我们小心处理;否则,我们可能会注意到内存泄漏的影响。
如果垃圾收集器工作正常,我们可以说,在给定的时间点,如果一个对象没有引用,垃圾收集器将找到该对象,它将假设不再需要该对象,因此,它可以回收该对象占用的内存。
那么,我们如何检测泄漏呢?windbg.exe 是在大型应用中查找内存泄漏的常用工具。除此之外,我们可以使用其他图形工具,如微软的 CLR Profiler、SciTech 的 Memory Profiler、Red Gate 的 ANTS Memory Profiler 等等,来查找我们系统中的漏洞。许多组织都有自己的内存泄漏工具来检测和分析泄漏。
在 Visual Studio 的最新版本中,有一个诊断工具可以检测和分析内存泄漏。它非常人性化,易于使用,你可以在不同的时间段拍摄不同的内存快照。工具中的标记表示垃圾收集器活动。这个工具的真正强大之处在于,您可以在调试会话处于活动状态时实时分析数据。图形中的尖峰可以立即吸引程序员的注意力。演示 2 包括执行以下程序后的快照示例。
演示 2
using System;
using System.Collections.Generic;
namespace AnalyzingLeaksWithSimpleEventEx1
{
public delegate string MyDelegate(string str);
class SimpleEventClass
{
public int ID { get; set; }
public event MyDelegate SimpleEvent;
public SimpleEventClass()
{
SimpleEvent += new MyDelegate(PrintText);
}
public string PrintText(string text)
{
return text;
}
static void Main(string[] args)
{
IDictionary<int, SimpleEventClass> col = new Dictionary<int, SimpleEventClass>();
for (int objectNo = 0; objectNo < 500000; objectNo++)
{
col[objectNo] = new SimpleEventClass { ID = objectNo };
string result = col[objectNo].SimpleEvent("Raising an event ");
Console.WriteLine(objectNo);
}
Console.ReadKey();
}
}
}
来自诊断工具的快照
这是诊断工具窗口的屏幕截图;它包括三个不同的快照,用于分析给定时间点的内存使用情况。
我们可以看到堆的大小是如何随着时间的推移而增长的。如果你仔细观察,你会发现我们在代码中注册了一个事件
SimpleEvent += new MyDelegate(PrintText);
但从未注销过。
我还用微软的 CLR Profiler 展示了一个案例研究,来分析与程序相关的内存泄漏。这个工具是免费的,非常容易使用(尽管目前它已经失去了其他工具的普及)。您可以下载 CLR 探查器(用于。NET Framework 4)来自 https://www.microsoft.com/en-in/download/confirmation.aspx?id=16273 (注意:在撰写本文时,该链接工作正常,但将来可能会被更改/删除)。
让我们分析与不同程序相关的泄漏,但在这种情况下,我们将使用 CLR profiler。
考虑下面的程序(我在程序完成执行后拍摄了快照):
演示 3
using System;
using System.IO;//For FileStream
//Analysis of memory leak with an example of file handling
/* Special note: To use the CLR profiler:
use the command: csc /t:exe /out:AnalyzingLeaksWithFileHandlingEx1.exe Program.cs to compile
General Rule: csc /out:My.exe File.cs <- compiles Files.cs and creates My.exe
(you may need to set the PATH environment variable in your system)*/
namespace AnalyzingLeaksWithFileHandlingEx1
{
class Program
{
class FileOps
{
public void readWrite()
{
for (int i = 0; i < 1000; i++)
{
String fileName = "Myfile" + i + ".txt";
String path = @"c:\MyFile\" + fileName;
{
FileStream fileStreamName;
try
{
fileStreamName = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
//using (fileStreamName = new //FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
Console.WriteLine("Created file no : {0}", i);
//Forcefully throwing an exception, so that we cannot close //the file
if (i < 1000)
{
throw new Exception("Forceful Exception");
}
}
// FileStream not closed
// fileStreamName.Close();
}
catch (Exception e)
{
Console.WriteLine("Caught exception" + e);
}
}
}
}
}
static void Main(string[] args)
{
FileOps filePtr = new FileOps();
{
filePtr.readWrite();
Console.ReadKey();
}
}
}
}
来自 CLR 探查器的快照
CLR 探查器的示例报告可能如下所示:
分析
从这个截图中,可以看到垃圾收集器需要清理不同代的次数。此外,如果您打开相应的直方图,您可以看到异常的问题与文件处理有关。为了供您参考,我在程序执行后打开了最终堆字节直方图、对象终结直方图和重定位对象直方图。
这是最终的堆字节直方图:
这是最终确定的对象直方图:
这是重新定位的对象直方图:
让我们修改程序
现在我们在前面的程序中启用了using语句,就像这样:
现在我们有了这份报告:
幸存对象的直方图如下:
重定位对象的直方图如下:
分析
现在我们可以看到不同之处:系统。FileStream 实例不再是一个问题。还要注意,垃圾收集器需要执行的任务比前一种情况少得多。
除此之外,您必须注意另一个重要的特征:如果我们分析 IL 代码,我们将看到一个 try/finally 块。
在这种情况下,编译器已经为我们创建了 try/finally 块,因为我们正在试验using语句。我已经提到过using语句充当 try/finally 块的语法快捷方式。
Points to Remember
根据微软的说法,using语句确保即使在调用对象上的方法时出现异常,Dispose()方法也会被调用。通过将对象放在 try 块中,然后在 finally 块中调用 Dispose()方法,可以获得相同的结果;实际上,编译器就是这样翻译using语句的。
所以,这一行代码:
using (FileStream fileStreamName = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
//lines of codes
}
转换成这个:
FileStream fileStreamName = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
try
{
//lines of codes
}
finally
{
if (fileStreamName != null) ((IDisposable) fileStreamName).Dispose();
}
老师继续说:让我们进入另一个讨论。在这种情况下,我们必须记住一个关键点:如果我们在 GC 中传递当前对象。SuppressFinalize()方法,则不会调用当前对象的 Finalize()方法(或析构函数)。
考虑以下三个程序及其输出,以理解我们如何在 C# 中回收内存。
演示 4
using System;
namespace GarbageCollectionEx1
{
class MyClass : IDisposable
{
public int Sum(int a, int b)
{
return a + b;
}
public void Dispose()
{
GC.SuppressFinalize(this);
Console.WriteLine("Dispose() is called");
}
~MyClass()
{
Console.WriteLine("Destructor is Called..");
System.Threading.Thread.Sleep(5000);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Exploring Garbage Collections.Example-1***");
MyClass myOb = new MyClass();
int sumOfIntegers = myOb.Sum(10,20);
Console.WriteLine("Sum of 10 and 20 is: " + sumOfIntegers);
myOb.Dispose();
Console.ReadKey();
}
}
}
输出
请注意,调用了 Dispose()方法,但没有调用对象的析构函数。
演示 5
现在我们已经注释掉了行//GC.SuppressFinalize(this);并且我们没有调用 Dispose()方法;也就是说,两行都被注释掉了。
using System;
namespace GarbageCollectionEx2
{
class MyClass : IDisposable
{
public int Sum(int a, int b)
{
return a + b;
}
public void Dispose()
{
//GC.SuppressFinalize(this);
Console.WriteLine("Dispose() is called");
}
~MyClass()
{
Console.WriteLine("Destructor is Called..");
//To catch the output at end, we are putting some sleep
System.Threading.Thread.Sleep(15000);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Exploring Garbage Collections.Example-2***");
MyClass myOb = new MyClass();
int sumOfIntegers = myOb.Sum(10, 20);
Console.WriteLine("Sum of 10 and 20 is: " + sumOfIntegers);
//myOb.Dispose();
Console.ReadKey();
}
}
}
输出
在这种情况下调用了析构函数方法。
演示 6
现在我们注释掉前面程序中的行//GC.SuppressFinalize(this);,但是调用 Dispose()。
using System;
namespace GarbageCollectionEx3
{
class MyClass : IDisposable
{
public int Sum(int a, int b)
{
return a + b;
}
public void Dispose()
{
//GC.SuppressFinalize(this);
Console.WriteLine("Dispose() is called");
}
~MyClass()
{
Console.WriteLine("Destructor is Called..");
//To catch the output at end,we are putting some sleep
System.Threading.Thread.Sleep(30000);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Exploring Garbage Collections.Example-3***");
MyClass myOb = new MyClass();
int sumOfIntegers = myOb.Sum(10, 20);
Console.WriteLine("Sum of 10 and 20 is: " + sumOfIntegers);
myOb.Dispose();
Console.ReadKey();
}
}
}
输出
Dispose()方法和析构函数现在都被调用。
恶作剧
如果你理解我们到目前为止讨论的程序,预测这里的输出。
注意,我们的程序结构类似于 GarbageCollectionEx1 唯一的区别是一个类包含另一个类。
using System;
namespace GarbageCollectionEx1._1
{
class MyClassA : IDisposable
{
MyClassB classBObject;
class MyClassB : IDisposable
{
public int Diff(int a, int b)
{
return a - b;
}
public void Dispose()
{
GC.SuppressFinalize(this);
Console.WriteLine("MyClass B:Dispose() is called");
}
~MyClassB()
{
Console.WriteLine("MyClassB:Destructor is Called..");
System.Threading.Thread.Sleep(5000);
}
}
public int Sum(int a, int b)
{
return a + b;
}
public int Diff(int a, int b)
{
classBObject = new MyClassB();
return classBObject.Diff(a, b);
}
public void Dispose()
{
GC.SuppressFinalize(this);
Console.WriteLine("MyClassA:Dispose() is called");
classBObject.Dispose();
}
~MyClassA()
{
Console.WriteLine("MyClassA:Destructor is Called..");
System.Threading.Thread.Sleep(5000);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Quiz:Exploring Garbage Collections.***");
MyClassA obA = new MyClassA();
int sumOfIntegers = obA.Sum(100, 20);
int diffOfIntegers = obA.Diff(100, 20);
Console.WriteLine("Sum of 10 and 20 is:{0}",sumOfIntegers);
Console.WriteLine("Difference of 10 and 20 is:{0}",diffOfIntegers);
obA.Dispose();
Console.ReadKey();
}
}
}
输出
恶作剧
现在让我们注释掉前面程序中的代码,如下所示:
会输出什么?
回答
分析
注意,这次调用了 MyClassA 的 Dispose()和 MyClassB 的析构函数。
摘要
本章回答了以下问题:
- 什么是垃圾收集(GC)?在 C# 中是如何工作的?
- 有哪些不同的 GC 代?
- 调用垃圾收集器有哪些不同的方法?
- 怎么才能强制 GC?
- 什么是内存泄漏?
- 内存泄漏的可能原因是什么?
- 怎样才能有效的使用 Dispose()方法来收集内存?
- 我们如何将内存泄漏分析与 Visual Studio 的诊断工具和微软的 CLR Profiler 结合使用?
十五、设计模式介绍
介绍
教师开始讨论:在一段时间内,软件工程师在软件开发过程中面临一个共同的问题。没有标准来指导他们如何设计和进行。当一个新成员(有经验或没有经验无关紧要)加入团队,并且他/她被分配从头开始做一些事情或者修改现有架构中的一些东西时,这个问题变得很重要。由于没有标准,理解系统架构需要巨大的努力。设计模式解决了这个问题,并为所有开发人员提供了一个公共平台。请注意,这些模式将在面向对象的设计中应用和重用。
大约在 1995 年,四位作者——Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 提交了他们的书《设计模式:可重用面向对象软件的元素》( Addison-Wesley,1995 年),他们在书中提出了软件开发中设计模式的概念。这些作者被称为“四人帮”。他们引入了 23 种设计模式,这些模式是基于软件开发人员长时间的经验开发出来的。现在,如果任何新成员加入开发团队,并且他知道新系统遵循一些特定的设计模式,他可以立即对该设计架构有所了解。因此,他可以在很短的时间内与其他团队成员一起积极参与开发过程。
现实生活中设计模式的第一个概念来自建筑建筑师克里斯托弗·亚历山大。他反复经历了一些常见的问题。因此,他试图以一种统一的方式用一个相关的解决方案(针对建筑设计)来解决这些问题。人们认为软件行业掌握了这个概念,因为软件工程师可以将他们的产品与构建应用联系起来。每个模式描述了一个在我们的环境中反复出现的问题,然后描述了该问题解决方案的核心,以这样一种方式,你可以使用这个解决方案一百万次,而不必以同样的方式做两次。—克里斯托弗·亚历山大
GoF 向我们保证,尽管模式是针对建筑和城镇描述的,但是相同的概念也可以应用于面向对象设计中的模式。我们可以用物体和界面来代替墙和门的原始概念。两者的共同点是,在核心上,两种类型的模式都试图在某些特定的上下文中找到一些解决方案。
1995 年,用 C++讨论了最初的概念。但是 C# 是 2000 年才出现的。在本书中,我们将尝试用 C# 来研究三种设计模式。如果你熟悉其他流行的编程语言,比如 Java、C++等等,那么你会很容易理解这些概念。我选择了简单且容易记忆的例子来帮助你发展这些概念。
要点
- 设计模式是针对常见问题的通用可重用解决方案。
- 我们的目标是制作一个如何解决问题的模板,可以在许多不同的情况下使用。
- 这些是对通信对象和类的描述,这些对象和类是为解决特定上下文中的一般设计问题而定制的。
- “四人帮”讨论了 23 种设计模式,它们可以分为三大类。
- 创建模式:这些模式抽象了实例化过程。我们试图创造一个独立于物体的构成、创造和表现的系统。以下五种模式属于这一类。
- 单一模式
- 原型模式
- 工厂方法模式
- 构建器模式
- 抽象工厂模式
- 结构模式:这里我们关注如何将类和对象组合成相对较大的结构。他们通常使用继承来组成接口或实现。以下七种模式属于这一类。
- 代理模式
- 轻量级模式
- 复合模式
- 桥接模式
- 立面图案
- 装饰图案
- 适配器模式
- 行为模式:这里我们关注的是算法和对象间的责任分配。我们也关注他们之间的交流过程。我们需要敏锐地观察这些物体相互联系的方式。以下 11 种模式属于这一类。
- 观察者模式
- 战略模式
- 模板方法模式
- 命令模式
- 迭代器模式
- 纪念品图案
- 状态模式
- 中介模式
- 责任链模式
- 访问者模式
- 解释程序模式
- 创建模式:这些模式抽象了实例化过程。我们试图创造一个独立于物体的构成、创造和表现的系统。以下五种模式属于这一类。
这里我们只探索三种设计模式:每一类一种。我选择了最简单的例子,以便你能容易地理解它们。但是你必须反复思考它们中的每一个,练习,尝试将它们与其他问题联系起来,并最终继续编写代码。这个过程会帮助你掌握这门学科。
单一模式
GoF 定义
确保一个类只有一个实例,并提供对它的全局访问点。
概念
一个特定的类应该只有一个实例。我们只会在需要的时候使用那个实例。
现实生活中的例子
假设你是一个运动队的成员。你的团队将在锦标赛中与另一个团队比赛。根据游戏规则,双方队长必须掷硬币来决定哪一方先开始游戏。所以,如果你的团队没有队长,你需要选一个人当队长。而且,你的队伍必须只有一个队长。
一个计算机世界的例子
在软件系统中,有时我们决定只使用一个文件系统。通常,我们用它来集中管理资源。
说明
在这个例子中,我们将构造函数设为私有,这样我们就不能以正常的方式实例化。当我们试图创建一个类的实例时,我们检查我们是否已经有一个可用的副本。如果我们没有这样的副本,我们将创建它;否则,我们将简单地重用现有的副本。
类图
解决方案资源管理器视图
下面显示了程序各部分的高级结构。
讨论
我们实现了一个非常简单的例子来说明单例模式的概念。这种方法被称为静态初始化。
最初,C++规范对于静态变量的初始化顺序有些模糊。但是。NET Framework 解决了这个问题。
这种方法的显著特点如下:
- CLR(公共语言运行时)负责变量的初始化过程。
- 当引用类的任何成员时,我们将创建一个实例。
- 公共静态成员确保一个全局访问点。它确认实例化过程将不会开始,直到我们调用类的实例属性(即,它支持惰性实例化)。sealed 关键字防止类的进一步派生(这样它的子类就不能滥用它),readonly 确保赋值过程将在静态初始化期间发生。
- 我们的构造函数是私有的。我们不能在外部实例化单例类。这有助于我们引用系统中可能存在的唯一实例。
履行
using System;
namespace SingletonPatternEx
{
public sealed class Singleton
{
private static readonly Singleton instance=new Singleton();
private int numberOfInstances = 0;
//Private constructor is used to prevent
//creation of instances with 'new' keyword outside this class
private Singleton()
{
Console.WriteLine("Instantiating inside the private constructor.");
numberOfInstances++;
Console.WriteLine("Number of instances ={0}", numberOfInstances);
}
public static Singleton Instance
{
get
{
Console.WriteLine("We already have an instance now.Use it.");
return instance;
}
}
//public static int MyInt = 25;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Singleton Pattern Demo***\n");
//Console.WriteLine(Singleton.MyInt);
// Private Constructor.So,we cannot use 'new' keyword.
Console.WriteLine("Trying to create instance s1.");
Singleton s1 = Singleton.Instance;
Console.WriteLine("Trying to create instance s2.");
Singleton s2 = Singleton.Instance;
if (s1 == s2)
{
Console.WriteLine("Only one instance exists.");
}
else
{
Console.WriteLine("Different instances exist.");
}
Console.Read();
}
}
}
输出
挑战
考虑下面的代码。假设我们在 Singleton 类中增加了一行代码,如下所示:
假设我们的 Main()方法如下所示:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Singleton Pattern Demo***\n");
Console.WriteLine(Singleton.MyInt);
Console.Read();
}
}
现在,如果您执行该程序,您将看到以下输出:
这是这种方法的缺点。在 Main()内部,您只尝试了 MyInt 静态变量,但是您的应用仍然创建了 Singleton 类的一个实例;也就是说,您对实例化过程的控制较少。每当您引用该类的任何成员时,实例化过程就会开始。
在大多数情况下,这种方法在. NET 中更受欢迎。
问答环节
问题 1:为什么我们要把事情复杂化?我们可以简单地编写我们的单例类,如下所示:
public class Singleton
{
private static Singleton instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
答:这种方法可以在单线程环境中工作。但是考虑多线程环境。在多线程环境中,假设两个(或更多)线程试图对此进行评估:
if (instance == null)
如果他们发现实例还没有被创建,他们每个人都会尝试创建一个新的实例。因此,我们最终可能会得到该类的多个实例。
问题 2:有没有其他的方法来模拟单例设计模式?
答:方法有很多。他们每个人都有自己的优点和缺点。让我们讨论其中的一种,叫做双重检查锁定。MSDN 将这一方法概述如下:
//Double checked locking
using System;
public sealed class Singleton
{
/*We are using volatile to ensure that
assignment to the instance variable finishes before it's access*/
private static volatile Singleton instance;
private static object lockObject = new Object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (lockObject)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
这种方法可以帮助我们在真正需要的时候创建实例。但是一般来说,锁定机构是昂贵的。
问题 3:为什么我们将实例标记为 volatile?
答案:我们来看看 C# 规范告诉我们的:“volatile 关键字表示一个字段可能会被同时执行的多个线程修改。声明为 volatile 的字段不受假定由单个线程访问的编译器优化的影响。这可确保字段中始终显示最新的值。”
简单地说,volatile 关键字帮助我们提供了一种序列化访问机制;也就是说,所有线程将按照它们的执行顺序观察任何其他线程的改变。请记住,volatile 关键字适用于类(或结构)字段;我们不能将它们应用于局部变量。
如果你有兴趣了解更多关于单例模式的不同方法,你可以看看 Jon Skeets 的评论。在他的文章 http://csharpindepth.com/Articles/General/Singleton.aspx 中,他讨论了各种选择(及其优缺点)来建立一个单例模式的模型。
适配器模式
GoF 定义
将一个类的接口转换成客户期望的另一个接口。适配器允许类一起工作,否则由于不兼容的接口而无法工作。
概念
下面给出的例子最好地描述了核心概念。
现实生活中的例子
这种类型最常见的例子是电源适配器。交流电源提供不同类型的插座,以适应所需的插座。考虑另一个例子。很多时候,我们需要通过总机用充电器给手机充电。但是,如果我们发现我们的移动充电器不能用于(或插入)特定的配电盘,我们需要使用适配器。在现实生活中,即使是翻译语言的译者也可以被认为遵循了这种模式。
因此,您可以这样想象:您的应用被插入到一个适配器(在本例中是 x 形的)中,该适配器使您能够使用预期的接口。没有适配器,您就不能连接应用和接口。
下图说明了使用适配器之前的情况:
下图说明了使用适配器后的情况:
一个计算机世界的例子
下面的例子很好地描述了这种模式最常见的用法。
说明
在这个例子中,我们可以很容易地计算出一个矩形的面积。请注意 Calculator 类及其 GetArea()方法。我们需要在 GetArea()方法中提供一个矩形来获取矩形的面积。现在假设我们想计算一个三角形的面积,但是我们的约束是我们想通过计算器的 GetArea()得到它的面积。我们如何做到这一点?
为了满足需求,我们为三角形制作了一个适配器(示例中为 CalculatorAdapter ),并在它的 GetArea()方法中传递一个三角形。该方法将三角形视为矩形,然后调用 Calculator 类的 GetArea()来获取面积。
类图
有向图文档
解决方案资源管理器视图
以下是该计划各部分的高级结构:
履行
using System;
namespace AdapterPattern
{
class Rect
{
public double l;
public double w;
}
class Calculator
{
public double GetArea(Rect r)
{
return r.l * r.w;
}
}
//Calculate the area of triangle using Calculator and Rect type as input.Whether we have Triangle.
class Triangle
{
public double b;//base
public double h;//height
public Triangle(int b, int h)
{
this.b = b;
this.h = h;
}
}
class CalculatorAdapter
{
public double GetArea(Triangle t)
{
Calculator c = new Calculator();
Rect r = new Rect();
//Area of Triangle=0.5*base*height
r.l = t.b;
r.w = 0.5*t.h;
return c.getArea(r);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Adapter Pattern Demo***\n");
CalculatorAdapter cal=new CalculatorAdapter();
Triangle t = new Triangle(20,10);
Console.WriteLine("Area of Triangle is " + cal.GetArea(t)+" Square unit");
Console.ReadKey();
}
}
}
输出
让我们修改插图。
我们已经看到了适配器设计模式的一个非常简单的例子。但是如果你想遵循面向对象的设计原则,你可能需要修改这个例子。一个主要原因是我们需要使用接口,而不是使用具体的类。因此,记住前面的目标,让我们修改我们的插图。
以下是新示例的主要特征:
- Rect 类实现 RectInterface,CalculateAreaOfRectangle()方法帮助我们计算矩形对象的面积。
- Triangle 类实现了 TriInterface,CalculateAreaOfTriangle()方法帮助我们计算三角形对象的面积。
- 您的约束是您需要使用 RectInterface 来计算三角形的面积。为了达到这个目的,我们制作了一个可以与 RectInterface 对话的适配器。
- 现在注意使用这种模式的好处:矩形和三角形代码都不需要改变。我们使用了一个适配器来帮助我们与 RectInterface 对话,在高层次上,似乎通过使用 RectInterface 方法,我们正在计算一个三角形的面积。
- 请注意,GetArea(RectInterface r)方法不知道通过 TriangleAdapter,它正在获取一个三角形对象,而不是一个矩形对象。
- 注意另一个重要的事实和用法。假设您没有很多矩形对象,但是您的需求很大。通过这种模式,您可以使用一些行为类似矩形对象的三角形对象。怎么做?嗯,如果你仔细注意,你会发现通过使用适配器(虽然我们调用的是 CalculateAreaOfRectangle()),它实际上是在调用 CalculateAreaOfTriangle()。因此,我们可以根据需要修改方法体;例如,我们可以将三角形面积乘以 2.0,得到 200 平方英尺的面积。单位(就像一个长 20 个单位,宽 10 个单位的矩形对象)。在您需要处理面积为 200 平方单位的对象的情况下,这可能会有所帮助。
- 为了更好的可读性,在这个例子中,我们没有遵循 C# 标准的接口命名约定(也就是说,我们没有以“I”作为接口的开头)。
解决方案资源管理器视图
以下是该计划各部分的高级结构:
履行
using System;
namespace AdapterPattern_Modified
{
interface RectInterface
{
void AboutRectangle();
double CalculateAreaOfRectangle();
}
class Rect : RectInterface
{
public double Length;
public double Width;
public Rect(double l, double w)
{
this.Length = l;
this.Width = w;
}
public double CalculateAreaOfRectangle()
{
return Length * Width;
}
public void AboutRectangle()
{
Console.WriteLine("Actually, I am a Rectangle");
}
}
interface TriInterface
{
void AboutTriangle();
double CalculateAreaOfTriangle();
}
class Triangle : TriInterface
{
public double BaseLength;//base
public double Height;//height
public Triangle(double b, double h)
{
this.BaseLength = b;
this.Height = h;
}
public double CalculateAreaOfTriangle()
{
return 0.5 * BaseLength * Height;
}
public void AboutTriangle()
{
Console.WriteLine(" Actually, I am a Triangle");
}
}
/*TriangleAdapter is implementing RectInterface
.
So, it needs to implement all the methods defined
in the target interface.*/
class TriangleAdapter:RectInterface
{
Triangle triangle;
public TriangleAdapter(Triangle t)
{
this.triangle = t;
}
public void AboutRectangle()
{
triangle.AboutTriangle();
}
public double CalculateAreaOfRectangle()
{
return triangle.CalculateAreaOfTriangle();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Adapter Pattern Modified Demo***\n");
//CalculatorAdapter cal = new CalculatorAdapter();
Rect r = new Rect(20, 10);
Console.WriteLine("Area of Rectangle is :{0} Square unit", r.CalculateAreaOfRectangle());
Triangle t = new Triangle(20, 10);
Console.WriteLine("Area of Triangle is :{0} Square unit", t.CalculateAreaOfTriangle());
RectInterface adapter = new TriangleAdapter(t);
//Passing a Triangle instead of a Rectangle
Console.WriteLine("Area of Triangle using the triangle adapter is :{0} Square unit", GetArea(adapter));
Console.ReadKey();
}
/*GetArea(RectInterface r) method does not know that through TriangleAdapter,it is getting a Triangle instead of a Rectangle*/
static double GetArea(RectInterface r)
{
r.AboutRectangle();
return r.CalculateAreaOfRectangle();
}
}
}
输出
注意
GoF 解释了两种适配器:类适配器和对象适配器。
- 对象适配器通过对象组合来适应。我们讨论的适配器是对象适配器的一个例子。在许多地方,您会注意到这个对象适配器的典型类图。
在我们的例子中,TriangleAdapter 是实现 RectInterface(目标接口)的适配器,Triangle 是被适配器。您可以看到适配器保存了 adaptee 实例(即在本例中实现了对象组合)。
- 类适配器通过子类化来适应。他们是多重继承的支持者。但是我们知道在 C# 中,不支持通过类的多重继承。(我们需要接口来实现多重继承的概念)。
下面是支持多重继承的类适配器的典型类图:
问答环节
问:如何在 C# 中实现类适配器设计模式?
答:我们可以子类化一个现有的类,并实现所需的接口。考虑下面的代码块。
class ClassAdapter : Triangle, RectInterface
{
public ClassAdapter(double b, double h) : base(b, h)
{
}
public void AboutRectangle()
{
Console.WriteLine(" Actually, I am an Adapter");
}
public double CalculateAreaOfRectangle()
{
return 2.0 * base.CalculateAreaOfTriangle();
}
}
但是我们必须注意,这种方法可能并不适用于所有场景;例如,当我们需要修改 C# 接口中没有指定的方法时。在这些情况下,对象适配器是有用的。
访问者模式
GoF 定义
表示要在对象结构的元素上执行的操作。Visitor 允许您定义一个新的操作,而不改变它所操作的元素的类。
概念
在这种模式中,我们可以将算法从它所操作的对象结构中分离出来。因此,我们可以向现有的对象结构添加新的操作,而无需修改那些结构。这样,我们就遵循了开放/封闭原则(允许扩展,但不允许修改实体,如类、函数、模块等)。).
现实生活中的例子
我们可以想象一个出租车预订的场景。当出租车到达我们家门口,我们进入出租车时,“来访”的出租车控制了交通。
一个计算机世界的例子
当我们插入公共 API 时,这种模式非常有用。然后,客户端可以使用访问类对某个类执行操作,而无需修改源代码。
说明
这里我们给出了一个简单的例子来描述访问者设计模式。你可以在这里看到两个类层次——最左边的一个代表原始的类层次。最右边的是我们创造的。IOriginalInterface 层次结构中的任何修改/更新操作都可以通过这个新的类层次结构来完成,而不会干扰原始代码。
考虑一个简单的例子。假设,在这个例子中,我们想要修改 MyClass 中的初始整数值,但是我们的约束是我们不能改变现有层次结构中的代码。
为了满足这一要求,在下面的演示中,我们将功能实现(即算法)从原始的类层次结构中分离出来,并将所有逻辑放入 visitor 类层次结构中。
类图
解决方案资源管理器视图
以下是该计划各部分的高级结构:
履行
using System;
namespace VisitorPattern
{
interface IOriginalInterface
{
void accept(IVisitor visitor);
}
class MyClass : IOriginalInterface
{
private int myInt = 5;//Initial or default value
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
}
}
public void accept(IVisitor visitor)
{
Console.WriteLine("Initial value of the integer:{0}", myInt);
visitor.visit(this);
Console.WriteLine("\nValue of the integer now:{0}", myInt);
}
}
interface IVisitor
{
void visit(MyClass myClassElement);
}
class Visitor : IVisitor
{
public void visit(MyClass myClassElement)
{
Console.WriteLine("Visitor is trying to change the integer value");
myClassElement.MyInt = 100;
Console.WriteLine("Exiting from Visitor- visit");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Visitor Pattern Demo***\n");
IVisitor v = new Visitor();
MyClass myClass = new MyClass();
myClass.accept(v);
Console.ReadLine();
}
}
}
输出
问答环节
问题 1:什么时候我们应该考虑实现访问者设计模式?
答:当我们需要在不修改现有架构的情况下添加功能时。这是访问者模式的主要目标。对于这种模式,封装不是主要考虑的问题。
问题 2:这种模式有什么缺点吗?
答:这里封装不是它的主要关注点。因此,在许多情况下,我们可能会使用访问者来打破封装。如果我们经常需要向现有架构添加新的具体类,那么访问者层次结构将变得难以维护。例如,假设我们想在原来的层次结构中添加另一个具体的类。在这种情况下,我们需要相应地修改 visitor 类的层次结构。
摘要
本章介绍了
- 设计模式
- 三个四人组设计模式:单体模式、适配器模式和 C# 实现的访问者模式