记一次AccessViolationException异常的排查

前几天,同事出差说程序遇到一个偶发的bug,程序会报告内存错误然后崩溃掉。今天终于有时间仔细排查了一下。

背景:这是一个C#和C++/CLI混合编程的程序。

1 复现

首先按同事描述的操作方法复现了bug,发现虽然是偶发的,单其实很有规律,在操作的某个时间点后必然出现。

2 基本原理

内存错误显然是非托管代码带来的,在.Net环境中被封装为AccessViolationException。

首先看看AccessViolationException的MSDN文档,它是”在试图读写受保护内存时引发的异常”。具体说明如下:

当非托管或不安全代码尝试读写未分配或不具有访问权限的内存空间时,就会产生访问冲突。 这种情况通常因为指针具有错误的值而发生。 并非所有通过无效指针的读或写操作都会导致访问冲突,所以访问冲突通常指示已经通过无效指针进行多次读或写操作,并且内存内容可能已损坏。 因此,访问冲突几乎总是指示存在严重的编程错误。 AccessViolationException 明确标识这些严重错误。

在完全由可验证托管代码组成的程序中,所有引用都有效或者为空,因而不会产生访问冲突。 AccessViolationException 只在可验证托管代码与非托管代码或非安全托管代码交互时才会引发。

如果发生异常在公共语言运行时,会保留的内存不足 .NET Framework 4开始,公共语言运行时本身引发的 AccessViolationException 异常没有被在结构化异常处理程序的 catch 语句处理。 为了处理这种 AccessViolationException 异常,则应用 HandleProcessCorruptedStateExceptionsAttribute 特性到引发异常的方法。 用户代码不影响 AccessViolationException 异常的此更改,可以继续用 catch 语句捕获。 对于针对要重新编译并运行,而无需修改.NET Framework 4.NET Framework 的早期版本编写的代码,则可以添加元素到 应用的配置文件。 注意也可能收到异常的通知,如果定义 AppDomain.FirstChanceException 或 AppDomain.UnhandledException 事件的处理程序。

看完先给发生问题的函数加上了HandleProcessCorruptedStateExceptions,捕获到异常了,但是没什么实际作用,这种错误明显不应该发生的。

3 发现错误源头

刚开始用VS2010调试,对C++/CLI太不友好了,看不到太多有用的信息,后来改用VS2015调试,能在崩溃时看到C++/CLI代码的详细信息了,然后发现了一个空指针,程序试图在空指针上进行结构体的访问,必然会崩溃啊。

对空指针的来源进行了仔细的回溯排查,发现在某个查找操作中返回了空指针,这个空指针还传递了好几个函数,衍生出好几个空指针,直到最后试图使用指针时崩溃了。

到这里发现了第一个问题,C++/CLI代码中的托管对象封装了传统C++的指针,对托管对象的操作,实际上会转发到指针上去,如果指针为空,这个托管对象实际上没有存在的意义,但是之前的代码忽略了这个问题。

在构建托管对象时,应当检验其封装的指针,如果为空指针,那么对象也应当初始化未null。

此时,AccessViolationException已经变成了C#程序员天天见的NullReferenceException,如果封装时注意一点,就不会这么辛苦找源头了。

4 出现空值的原因

产生空值的查找操作,按理说不应当出现,而且它的出现也是有一些偶发性的,如果把异常截获,程序还能继续跑下去也没什么问题,也就是,待查询的对象集合在大部分时间都是正常的,但在某个瞬间出了问题。

仔细排查了一下哪些操作可能更动这个对象集合,发现了一个重启操作,包含了清空与加载两步,而且这个重启操作与出问题的查询操作在不同的线程中。

到这里,发现原来是典型的操作原子性未保持导致的多线程错误,清空后还没加载时去查询,自然会出现空值,给对象集合所在对象加了个锁总算没有异常了。当然,后来我发现这个重启的必要性很可疑,那是另外的话题了。

5 小结

总计一下:

  1. 应当让错误早点暴露,限制空指针的传播范围,具体到C++/CLI中,要把它封装成null。
  2. 多线程的坑层出不穷,行为难以预料,要谦虚谨慎。

屁股决定脑袋

有句说出来不甚雅观的俗语叫“屁股决定脑袋”,我近几年越来越觉得这句话很有道理。我亲眼看见某些人对同一件事情的看法发生天翻地覆的变化,只是因为他前后两次所处的地位不一样。

每个人倾向于将自己的思想和行为变得融洽,而一个人所处的位置往往决定了他会有哪些行动,所谓“人在江湖,身不由己”,即使刚开始,他可能不大情愿去做某些事,但是时间长了次数多了,他必然要修正自己的三观去匹配自己的行为,这就是行为反作用于思想的可怕之处。如果不修正自己的三观,那么他就面临时时刻刻的矛盾,自己不认同自己所做的事情,陷入痛苦不堪的境地。电影《朗读者》中的女主角参与了屠杀犹太人,但她只把那当成了自己的日常工作,她不会让自己意识到自己的行为有多么罪恶,这就是典型的屁股决定了脑袋。

还有句俗话叫“不要因人废言”,但是仔细想想,一句话从谁嘴里说出来与这句话的可信度还真是有很大的关系,我们首先得看看说这句话得人屁股在什么地方,看清楚他得利益所在,再去判断这句话有没有意义,比如我们为什么会觉得“五毛”和“美分”说的话不可行,因为他们拿钱发帖,显然不会发自真心的去考究自己的话。又比如罗振宇在跨年演讲时说了一堆趋势,可是仔细想想他的屁股在自媒体和知识服务上,所以他的说法都是对应这些来说的,那么我们作为听众就得看清楚自己的屁股可能不再那里,所以参考价值可能比想象中低多了。

超越自己的屁股相当困难,至少有两个障碍,一是认知上的障碍,每个人的视角都是有限的,观点自然都是从自己熟悉的领域出发,向外辐射时肯定会有局限,二是利益上的障碍,少有人会发表对自己所处地位不利的观点,即使他能分辨所谓的善恶对错,但是谈论的可能还是利弊,更可怕的时他还会修正自己的三观去适应自己的利益。

所以,要想少被人在脑子里跑马,听人说话时得看看他的屁股在哪里。而自己的思想想要超出自己的位置,那是需要反复的修炼的,就像《无间道》中道明叔说的:从来都是事情改变人,而少有人改变事情。通过改变思想改变自己的行为和位置很难,而因为自己处在一个位置就改变了思想则是不知不觉的。

五步阅读法

简要总结一下自己最近的阅读方法,还需要不断迭代更新。

阅读的基本目标应该是知行合一,所以以娱乐为目的的阅读不在本文讨论范畴。本文适用于读书,也适用于阅读中短篇的文章。

第一步 速览

这一步是专门针对我这样的急性子而设定的,拿到一本好书,总是会火急火燎的想把它看完,虽然仔细想想,怎么才算把一本书看完,也是很需要思考的重大问题。所以,如果实在克制不了,也不妨放自己一马,先快速读一遍,也算是留个大概印象。另外一种情况是快速读一遍之后发现这本书不值得细读,也算是节约自己时间了。从这个角度出发的话,速读也大可不必,速览一遍,观其大概就行了,当是挑书的一个过程。

第二步 演读

不知道这个词是不是我生造的,我的意思是详细的去读书,详细到作者的思路能在脑子里演绎,如果不能达到这个程度,可能有几个原因:

  1. 你的理解能力不够,背景知识不够,这本书还没到你读的时候;
  2. 你需要多读几遍;
  3. 这是本烂书。

如何分辨这三种情况,实在是一个困难的事情,需要不断的积累经验,一般而言一个领域懂得越多,分辨起来就会越轻松。

演读的目的在于真正把书的内容吃透,理解作者的思想。

注意这个过程最好不要做笔记,更不要画重点,不要用手头的忙碌掩盖思维的懒惰,做出一副认真读书的样子自欺欺人。

第三步 提取

把书合上,开始做笔记,根据书的信息容量,看完一部分还是全部看完再做这一步可以灵活把握。

合上书做笔记,是一种简单而有效的自我检查手段,它要求你把学到的东西再反馈到纸上,这个笔记可以是提纲挈领式的,只要能根据这些提要把所记忆理解的东西复述出来,当然有时间的话还是越详细越好。实在没有时间或条件,至少合上书,自问自答一番。

做完笔记,可以再翻开书比对一番,看是否有错误与遗漏。

第四步 连网

要想链牢固的掌握所阅的东西,关键是把它转化为自己知识体系的一部分,将它链接到已有的神经网络中去。有如下基本基本的方法。

  1. 比较:跟已经掌握的知识做比较,看看有哪些异同点,是不是已有知识的另外一个面目,或者一个重要的补充;
  2. 反思:书中所说的有没有不准确的地方,有没有不详细的地方,或者能不能换一个更易于理解的说法。

第五步 践行

这是最后一部,也是最重要的不能省略的一步,尤其是对于有方法论指导的书而言,如果一本书中有一个重要的实践手段得到了你的理解认同,那么最好就是照着去做,这是阅读的最佳归宿,一本书能有这么一个点就是价值百倍了。

当然,不是所有书都有直接的方法论可供实践的,那么可以有几个退而求其次的办法:

  1. 读书笔记,这个读书笔记最好是对第三步笔记的改进版,用你自己的理解去重新整理,或者添加了你自己的理解,或者用学到的道理去阐释一个新的问题。
  2. 交谈,向一个没看过这本书的人去介绍其中的内容,根据不同的交流对象,选择合适的方式和内容,但是传递同样的思想内核。

到这里,大概算是读完了一本书。

最后要说明的是这五个步骤不是严格割裂和先后排序的,具体应用时可以灵活裁剪及交叉进行,但是最后一步是不能省略的,否则就只能算是娱乐性阅读,即使你读的是相当严肃的书。

高可配置性软件开发

可配置性是软件工程实践中的重要问题之一,配置文件与可执行文件一起构成了软件运行时版本控制的要素,Visual Basic,Python、Lua等解释型语言,常以所谓“脚本”的形式出现,进一步提升了操作系统、游戏等大型软件的可配置性,而领域驱动设计在强调领域模型的同时,也带动了描述配置领域模型的领域特定语言的发展。在工业控制领域,软件的可配置性用组态的概念来描述;在游戏开发领域,一般用引擎和脚本的概念来描述。

1 基本原理

软件复杂性的根源在现实世界的复杂性,软件的复杂性超过人的理解能力时,会带来软件开发的困难乃至失败,好的软件工程方法论能给出现实世界的良好抽象模型,通过合理的抽象层次结构管理软件的复杂性,使人能从整体到细节分层次的把握软件。源自现实世界的复杂性并不会降低或消失,但却得到了约束。

高可配置性可在一定程度上解决快速需求变更问题,把握软件中不变的部分作为模型,将易变的部分通过配置文件等方式约束起来。

highly-configurable-1

2 构建方法

构建高可配置性软件,首先应当针对领域特点建立通用软件模型,然后针对模型使用配置技术。不同的配置加上模型最后构建了不同的软件实例。

highly-configurable-2

2.1 建模

建模问题是一个很复杂的问题,取决于设计时不同的场景,需要领域知识与设计经验,并且和领域专家密切合作。这方面有几本经典的书:《领域驱动设计》《实现领域驱动设计》

2.2 配置技术

2.2.1 配置文件

配置文件是使用最广泛也是最基本的一种配置技术,配置文件主要用于描述软件启动及运行时所需要的数据,一般不直接描述软件的运行逻辑,常见的配置文件格式包括ini、xml、json等,简单比较如下表所示:

格式 表达能力 可读性 典型应用场景 举例
ini 较弱,不可嵌套 最好 软件启动参数。 启动目录、ip地址。
xml 最强 最差 复杂对象的序列化。 数据帧结构与解析规则。
json 一般 一般 web API接口。 天气预报服务的接口。

2.2.2 脚本语言

一些大型的程序,往往通过结合多种编程语言来实现,一般通过编译型语言实现系统的相对固定的部分与运行效率关键部分,通过解释型语言来实现系统的易变部分。比如Linux操作系统用C实现了很多工具,而通过bash脚本对这些工具进行综合应用,大型游戏基于C++游戏引擎构造,而使用Lua脚本等描述游戏动作逻辑。

2.2.3 领域特定语言

领域特定语言介于一般的配置文件和程序代码之间,即针对某一特定领域,具有受限表达性的一种计算机程序设计语言。

领域特定语言又分为内部领域特定语言、外部领域特定语言、语言工作台等,其中内部领域特定语言是主要语言程序代码的一种特殊写法,不能算是配置,外部领域特定语言则设计新的语言来进行表达,仍可归为文本配置的范围。而语言工作台则主要提供可视化的配置方式或者成为可视化编程。

领域特定语言介于配置文件和脚本程序之间,提供了领域内较高的表达能力,也便于领域专家的理解,可加速专家知识向软件的生成。

针对不同模型的需要,以上3种配置技术在高可配置性软件开发均可应用,下表对三种配置技术的主要特点和典型应用场景做了比较。

配置技术 表达能力 可读性 典型应用场景 举例
配置文件 描述软件运行需要的数据,一般不直接描述软件运行逻辑。 取决于具体的配置文件格式。 软件启动参数。 启动目录、网络通信地址。
脚本语言 直接描述软件的运行逻辑。 仅适合程序员理解,不利于领域专家理解。 基本接口形式固定时,描述易变逻辑。 在C++构建的设备监控接口基础上用Python脚本描述监控流程。
领域特定语言 描述领域模型的配置,从而间接描述软件运行逻辑。 既适合程序员理解,也适合领域专家理解。 建立了认可度较高的领域模型,模型的配置丰富易变。 用“状态-反应”模型表现故障处理,建立领域特定语言描述具体的内容。

3 工程实践

如图所示,高可配置性软件开发实际改变了传统软件的开发流程,突出了软件配置在软件生命周期中的重要地位。下面将阐述高可配置性对传统软件开发各阶段带来的影响。 highly-configurable-3

3.1 需求分析

在需求分析阶段,相比普通开发,高可配置性软件开发需要再重点分析以下问题:

  1. 采用高可配置性技术的必要性,这主要是由软件的适用范围决定的,即软件是否要具备可推广性,软件应对的需求是否易变。软件需要适配的场景越多,需求越易变,则必要性越大,如果软件只用于一个基本固定的场景,则没有必要使用配置。
  2. 区分模型需求与配置需求,在需求采集时,要采集软件在各种应用场景的需求,分析软件可能因推广带来的需求扩展,尽可能分析较多的案例,深入分析提取系统的不变性,抽象出系统的模型,而将系统的易变部分识别为配置需求。

3.2 软件构建

高可配置性软件开发的软件构建分为两个阶段:

  1. 软件主体构建:该阶段由程序员完成,高可配置性对架构设计要求较高,在使用面向对象范式时,需要深刻理解与应用面向对象的设计原则,大量应用各类构件型设计模式,以及反射、依赖注入等运行时关联技术;
  2. 软件配置阶段,该阶段可由程序员完成,也可由领域专家完成。由于配置成为软件的重要组成部分,所以对配置文件应当和对源代码一样严格管理,纳入版本管理系统,由于人工配置容易产生错误,所以应当尽量提供可视化的配置工具,限制用户随意修改配置。

3.3 软件测试

对应软件的两阶段开发,高可配置性软件的测试也分为两个部分:

  1. 软件主体测试,构造测试用例应当设计各种场景的配置,这些场景要考虑模型的边界情况,通过测试尽量消除软件主体的缺陷,减少软件推广应用时在配置后发现缺陷。

  2. 软件配置测试,完成配置测试才能在具体的场景应用,配置过程往往由领域专家而非程序员完成,因此最好对配置进行校验,并提供配置完成后的测试工具与测试方法细则,使得领域专家也能完成软件的测试。

3.4 软件维护

对应软件的两阶段开发,高可配置性软件的维护也分为两个部分:

  1. 软件主体维护,通用的功能升级通过对软件主体的升级完成,软件主体功能的修改应当保持配置的兼容,如果不得不改变配置的格式时,应当提供配置的升级转换功能;

  2. 软件配置维护,用户需求与应用场景发生了变化,而这些变化在软件主体预期之内时,可由领域专家修改配置完成,软件配置逐渐成为用户的重要资产。

如何创建一个对象(C#篇)

在面向对象编程范式的语言中,对象是最重要的一个概念,对象维护自身的状态,通过发消息(调用)来影响其他对象,从而完成整个程序的运转。何时以何种方式创建对象,是面向对象设计中需要重点考量的问题之一。本文对C#语言中创建对象的方法进行简要总结,涉及语言特性、设计模式等,所有的例子以能说明清楚问题为目的,不注重其实用性。相信本文对Java等其他面向对象语言也有相似的价值。

1. new与构造函数

一个C#对象封装了一些字段和一些方法(属性、事件等本质上都是方法),考察一个C#对象的内存图景可以发现,每个具体的对象实际上拥有的是字段,而方法本质上都是全局性的,对象上方法的调用A.b(...)可视为b(A,...),对象的this指针将作为方法的一个参数传入,从这个角度理解就不难发现,创建一个对象本质上需要根据对象的字段划分一块内存,并将这些字段的内容初始化。完成这项工作最直接的途径就是new与构造函数,这也是C#语言提供的构建对象的基本机制。

考虑下面这个简单的例子,Shape类主要抽象了计算面积这一行为,派生类RectangleCircle具体完成这一行为。

public abstract class Shape
{
    public abstract double Area();
}

public class Rectangle:Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }
    public override double Area()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(double radius)
    {
        Radius = radius;
    }
    public override double Area()
    {
        return System.Math.PI*Radius*Radius;
    }
}

public class Test
{
    public static void Main()
    {
        Shape rectangle = new Rectangle(3, 4);
        Shape circle = new Circle(5);
        System.Console.WriteLine(rectangle.Area());
        System.Console.WriteLine(circle.Area());
    }
}

我们可以通过如下命令编译与查看IL代码

csc Shape.cs
ildasm Shape.exe

Test类中Main函数的IL代码如下:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // 代码大小       65 (0x41)
  .maxstack  2
  .locals init (class Shape V_0,
           class Shape V_1)
  IL_0000:  nop
  IL_0001:  ldc.r8     3.
  IL_000a:  ldc.r8     4.
  IL_0013:  newobj     instance void Rectangle::.ctor(float64,
                                                      float64)
  IL_0018:  stloc.0
  IL_0019:  ldc.r8     5.
  IL_0022:  newobj     instance void Circle::.ctor(float64)
  IL_0027:  stloc.1
  IL_0028:  ldloc.0
  IL_0029:  callvirt   instance float64 Shape::Area()
  IL_002e:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_0033:  nop
  IL_0034:  ldloc.1
  IL_0035:  callvirt   instance float64 Shape::Area()
  IL_003a:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_003f:  nop
  IL_0040:  ret
} // end of method Test::Main

在IL代码中,构造函数编译为一个名为.ctor的方法,而new操作有对应newobj操作符,生成一个对象实例后将其加入Main函数的栈,而调用对象方法时的操作callvirt则保证了在运行时根据继承树找到合适的方法。

很容易发现,普通方法Area是运行时确定的,但是构造函数却不得不在编译时确定,或者说,对象的使用是多态的,对象的创建则是具体的,因此,对象的创建,或者说new与构造函数这个机制,成为制约程序变化的瓶颈。

2 new的隔离与封装

基于1中的例子,考虑如下的需求,用户从命令行输入参数信息,第一个参数代表形状的种类,后续参数代表构造这个形状需要的数据,然后由程序计算面积。代码如下:

public class Test
{
    public static void Main(string[] args)
    {
        Shape shape = ShapeFactory.CreateShape(args);
        if(args[0]=="Circle")
        {
          shape = new Circle(double.Parse(args[1]));
        }
        else if(args[0]=="Rectangle")
        {
          shape = new Rectangle(double.Parse(args[1]),double.Parse(args[2]));
        }
        if(shape!=null)
        {
          System.Console.WriteLine(shape.Area());
        }
    }
}

考虑以下两点原因,我们需要进一步封装上述代码中的条件分支语句:

  1. 在应用Shape这个体系的时候,类似的根据不同的字符串来创建不同的子类的代码还可能很多,比如根据配置文件来创建Shape,根据数据库中的数据来创建Shape等,需要消除重复代码;
  2. Shape的种类可能不断增多,需要隔离变化。

封装出的方法CreateShape可以作为Shape类的静态方法,或者作为一个新的工厂类ShapeFactory的静态方法。

public class ShapeFactory
{
  public static Shape CreateShape(string[] args)
  {
    Shape shape = null;
    if(args[0]=="Circle")
    {
      shape = new Circle(double.Parse(args[1]));
    }
    else if(args[0]=="Rectangle")
    {
      shape = new Rectangle(double.Parse(args[1]),double.Parse(args[2]));
    }
    return shape;
  }
}

public class Test
{
    public static void Main(string[] args)
    {
        Shape shape = ShapeFactory.CreateShape(args);
        if(shape!=null)
        {
          System.Console.WriteLine(shape.Area());
        }
    }
}

这一般称为简单工厂,算是一种准设计模式,代码的扩展点位于CreateShape中的条件分支语句。

3 工厂方法和抽象工厂

前述2中的ShapeFactory是一个具体类,如果提取出一个接口IShapeFactory来,二者就构成了一个工厂方法模式,不过这么做没什么意义,工厂方法更多的是体现在平行的类层次上,考虑这样的需求,假设我们在一个编辑环境中编辑这些Shape,双击它们的会弹出不同的设置窗口,即对不同的Shape有不同的Editor,这时候可以在抽象类Shape中加入一个工厂方法CreateEditor,它的具体创建行为延迟到子类中决定。

public abstract class Shape
{
    ...
  	public abstract Editor CreateEditor();
}

public class Rectangle:Shape
{
    public override Editor CreateEditor()
      {
        return new RectangleEditor(this);
      }
}

public class Circle : Shape
{
    public override Editor CreateEditor()
      {
        return new CircleEditor(this);
      }
}

public abstract class Editor{}
public class RectangleEditor : Editor{}
public class CircleEditor : Editor{}

具体的创建行为在子类中,但是它们的关系依然是编译时决定并绑定死的。

抽象工厂可以视为工厂方法的扩展,假如Shape除了构造Editor类体系外,还构建自己的3D形式,那么需要在Shape中再加入一个Create3D方法,返回ThreeDShape,这时Shape类构建了一系列相关的类,成为一个抽象工厂,当然这时Shape类太过复杂,可以考虑把这些创建函数都提取到ShapeFactory中去,这里就不再赘述了。

4 反射工厂

回到1中的ShapeFactory,它的扩展点主要是根据不同的字符串生成不同的类,这是创建对象时最常见的场景之一,这个字符串往往来自于用户配置,如果事先知道有多少种子类,那么这个条件分支已经是程序中唯一的扩展点了,也是可以接受的,但如果事先不知道有多少种子类,比如我们把Shape类作为一个库提供给程序员使用,那么ShapeFactory的位置就会比较尴尬,而.Net的反射机制则能一劳永逸的解除这个尴尬。

根据一个字符串,创建一个同名的对象是很自然的想法,但在C++中,很难实现,因为C++的类编译后不再保有自己的类名信息,而.Net则在运行时一直保有类的详细信息。使用反射改写工厂方法如下:

public class ShapeFactory
{
  public static Shape CreateShape(string[] args)
  {
    System.Type type = System.Type.GetType(args[0]);
    object[] shapeArgs = new object[args.Length - 1];
    for(int i = 0; i < args.Length - 1; i++)
    {
      shapeArgs[i] = double.Parse(args[i+1]);
    }
    return (Shape)System.Activator.CreateInstance(type, shapeArgs);
  }
}

反射是C#和Java这类静态语言获得一定动态性的重要机制,在一些动态语言如Python中,由于Eval机制的存在,字符串和代码基本上是不分家的。

5 序列化与反序列化

简单理解的话,序列化等同于把运行时对象存储为一个字符串,而反序列化则等同于从这个字符串(常使用xml格式)恢复对象。一般可以用工厂方法来实现。比如可以这样设计Shape的反序列化。

public abstract class Shape
{
    ...
  	public static Shape Load(string str)
    {
      ...
    }
  	public abstract string Save();
}

Load方法中,由于要根据字符串来辨别子类,所以离不开反射机制。

.Net提供了比较友好的序列化与反序列化机制,只要我们设计类型时遵循一定的原则,比如需要无参构造函数等,就可以直接使用该机制。比如在Rectangle中加入无参构造函数后,可以把它保存到xml文件再恢复出来。

Shape rectangle = new Rectangle(3, 4);
XmlSerializer mySerializer = new XmlSerializer(typeof(Rectangle));
StreamWriter myWriter = new StreamWriter("rectangle.xml");
mySerializer.Serialize(myWriter, rectangle);
myWriter.Close();
FileStream myFileStream = new FileStream("rectangle.xml", FileMode.Open);
Shape rectangle2 = (Rectangle)mySerializer.Deserialize(myFileStream);

由于序列化和与反序列化机制的成熟,用类似上面的代码可以轻松的实现对象克隆,所以原型模式在C#中比较少使用。

一般的Xml序列化有一个问题,它不能保持对象间的引用关系,微软还将WPF中的Xaml序列化单独拿出来做成了库,所以不想自己实现一个保持引用关系的序列化机制的话,可以考虑System.Xaml 命名空间。

6 依赖注入

关于依赖注入的原理,看懂老马的这篇文章就够了。

依赖注入要解决的是一系列相关对象的创建问题,对象A的组成部分之一B是一个抽象类,那么在创建A的时候就免不了要创建B的实例,而这就免不了具体类的出现,因此需要使用工厂类完成这项工作,这样类A才能真正和B的具体子类解耦。依赖注入通过一个公有的的工厂机制来解决这个问题,只要你的类按照一定的规则设计,比如提供构造函数或者提供属性,用于设定类中包含的抽象类型的具体实例。

依赖注入框架一般支持代码创建对象,也支持xml配置文件创建对象,第一种方式将对象创建代码集中起来,但对象类型仍然是编译时确定的,而第二种方式则借助反射,使用描述式编程来管理对象创建,完全实现运行时确定对象类型。

C#中的依赖注入框架,除了官方的Unity外,还有Autofac,Ninject等。

对象的创建问题,到依赖注入框架,基本上也就到极致了。

写作与跑步

前几天报名了马拉松,准备跑个半马,不辜负这美好的空气。昨天晚上做了第一次热身,跑了十二公里,跑下来比较轻松,只是在大约八公里的地方腿酸得不行,一副不停下来就要断掉的架势,好在我还了解一点运动理论,明白这个时候大脑是在提前预警而已,实际上人的潜能远不止如此,所以感受着酸爽,坚持了一会儿,那种感觉也就过去了。

今年我发了宏愿,要每天更新一篇博客,写了十几天之后,就感觉快要江郎才尽,无话可说了,这就像一个没怎么跑过步的人,上来跑了一公里就气喘吁吁一样。长跑的人都知道,在超越体能极限的时候是不能停下来的,即使步频和步幅会有所下降,也要保持动作的规范,不能从跑变成走,否则就难以超越极限,体会到长跑特有的那种乐趣。

昨天我跑到第八公里的时候,还想到了我的博客,乃至我的生活,在极限的压力逼迫时,你的思考可能不再锐利,但是唯一不能忘记的是迈动你的双腿,,这个时候技巧不是最重要的,最重要的是坚持。所以我的的博客每天更新一篇要怎么坚持下去呢?当然需要以输出带动输入,学习整理很多东西,这是我要做这件事的初衷。可是最简单也最重要的一点是什么呢?是无论发生任何情况,每天一篇坚持写下去,即使质量不高,即使绞尽脑汁。

这几年我研究了不少方法论的东西,发现可能最重要的方法论之一是形式主义的坚持实施,就是把一种形式落到实处,坚持不懈,当然形式对实际的促进作用程度不一,但大多数人的问题不是形式不好,而是根本没有坚持,正如跑步姿势再标准,坚持不下去的化,也无法完成马拉松一样,看过了无数的学习英语的技巧,但却学不好英语。太多的小聪明之外,忘记的是笨办法的伟大之处。

在极限中迈开双腿,这是长跑的秘诀,是长跑带给人生的启示,也正是长跑的魅力所在吧。

干货的问题在哪里

在我们湖南老家,夏天的时候,一场雨水一天阳光,地里的蔬菜就会噌噌噌的长,多到吃不完,那个时候,我妈就会把黄瓜、葫芦瓜、豆角等等简单处理一下后都拿去暴晒,做成菜干,冬天的时候,放到腊肉火锅中,某种程度上菜干是蔬菜味道的精华,所以吃起来相当美味。

不知道从什么时候开始,人们看书看文章也喜欢起干货来,经常看到有人会评价说,这本书不就是说了一个什么什么道理么?需要说这么多页么?我用100个字就概括了。当然不排除有一些书会为了多卖几块钱而注水,但是很多时候情况不是这样。

一本书往往只写了一个主题,或者讲了一个故事,比如《断舍离》想要教你如何反思消费主义,精简自己的物品,《西游记》讲的是唐僧师徒西天取经,结果他们去到了真经,都成佛了,这两本书的书名都很好,看书名就知道了书的主要意思,如果要看最干的干货,看书名是不是就足够了呢?阐释和详述的意义到底在哪里呢?

吃蔬菜时吃进去的水分究竟有什么意义呢?

为了迎合爱干货的人,现在像《罗辑思维》之类的还提供了微缩书的服务,把一本书的干货挑给你看,看这样的浓缩和看原书有什么区别呢?我觉得有两个区别。

一个主要的区别在于细节可以带来可信度,回归到我们看书的目的,对于非虚构类的书一般带有一定功利性,我们一定是想解决某些问题或者获得某些思想采取读书的,如果我们看的书没有值得我们去相信的东西,那显然没有看的必要,但是缺乏细节会使得我们只是在眼前晃了一些概念,而没有真正理解这些概念,把它们训练成大脑中的一种模式,因此去践行这些方法或思想的可能性就很小,看干货的结果就是看的时候觉得说得似乎都挺好,但也仅此而已了,看虚构类的书就更需要细节了,否则就完全沦为了装逼行为,而不是从故事中去寻找愉悦与精神享受。

另外一个区别在于细节带来的意外启发,一本好的书是作者的思想流露,尽管它有主要的话题,但作者往往也会偏题,这种偏题可能带给读者意外的收获,比如喜欢美食的人看《红楼梦》可能学会了做几道菜,看育儿的书提高了沟通能力,改善了自己的社交关系。

干货存在的意义除了给假装努力的人安慰外,还可以给真正努力的人一个目录,但也仅此而已。

真正好的东西值得反反复复从不同的角度去阐释,正如蔬菜还是带着水更原汁原味一样。浓缩的是精华,但也可能是一厢情愿和消化不良。

货币的现金价值

1 现值和终值

终值取决于现值、利率和复利次数,而且复利次数的作用比想象中还要重要,所以理财越早越好。

反过来计算,未来的钱是有一定画饼性质的,因为通过膨胀率一般都是正的,这时候称为贴现率,中国的通货膨胀率 近30年平均约为6%。

2 年金与定投

终值取决于每年支付额、利率、复利次数(年份)。

反过来可以根据终值、年份和利率计算每年支付额。

比如,30年后想要有1000万继续,年化利率按10%计算,那么每年要攒多少钱呢?

10000000*0.1/((1+0.1)**30-1)
60792

每年攒6万多就好了,坏消息是,30年后的1000万,按贴现率6%计算,只相当于

10000000/(1+0.06)**30
1741101

现值174万,所以说通胀猛于虎啊。

如果能早攒10年钱呢?还是每年攒6万多,年化利率10%,那么终值就会变成

60792*(1.1**40-1)/0.1
26906086

不止翻了一倍啊!

所以理财要趁早啊,不过现代人的寿命越来越长,理财的作用也会越来越明显。

3 分期付款

年金的现值

反过来计算分期付款额度(等额本息)

比如300万的房贷,年利率5%,还30年,则月供

(3000000*0.05/(1-1.05**(-30)))/12
16262

1.6万。

我读了几本书?

读书这回事,我常说,跟打麻将一样,算是个人爱好,并不见得高人一等。可是这种说辞有点自我催眠的意思,因为下意识里还是会把读书当成一个有些追求的活动,可能有两个原因,一是因敬畏知识而敬惜字纸,二是消解内心中渴望进步的焦虑。

最近几个月重看了好几本书,书上的字词自然是没有变化,但是对书的理解却好似之前没读过一般,因为这些重读的书中的很多道理或者做法,我已经有一些行动的经验了,此时带来的共鸣自然不可同日而语。

读虚构类的书是一种享受,一般而言还上升不到行动。而读非虚构类的书,则需要判断它说得有没有道理,方法可不可行,如果答案是肯定的,那么就得照着去做,否则读它作甚呢?读了书,觉得讲得好,而没有去做,只不过是一次智力上的自亵,然而并没有什么卵用,用在这里算是合情合理了。

很多年前,我看读者还是青年文摘的时候,看到一篇鸡汤文,说某个穷苦人,因为看了读者还是青年文摘中的鸡汤文,发奋图强,成了小有名气的破乱大王,我想,她真的是很牛,看了一篇文章就发财了,我看了那么多本书,什么时候能发财呢?我现在愿意相信这个鸡汤文是真的,好用的道理也许真的只要一个就够了,好用的道理往往是简单的道理,但是坚持践行却很难,比如巴菲特不会盯着股价炒股而是去研究企业来投资,这么简单的道理真正能做到的又有几人?中国古话讲知易行难,大致如此了。

我算的上是一个喜欢看书的人,但是又顶瞧不上眼高手低、百无一用的书生,书生的臭毛病在于认为读了书就高人一等,本质上跟背了个名牌包包就觉得自己高人一等的拜金女一样,我们湖南农村有句话叫“读书读到屁眼里去了”,我有时候也不禁想想,我把书读到哪里去了呢?

今天我在重读某本书的时候,发现书中的道理我非常认同,可是几年前读我也大概是认同的吧,但是这一次我有了强烈的要去行动的愿望,同时也不禁汗颜,因为我已经在这点差异之间,浪费了几年的时间。

所以,最后我能很好的回答标题的问题了,读了几本书根本不重要,重要的是践行了多少?正如下赌场打麻将,打了几盘不重要,重要的是赢了多少钱。我听过了无数遍知识就是力量这个道理,但直到今天,才明白从知识到力量的过程中,还有一剂催化剂,缺了它,读书就真的跟打麻将一样了。

磨洋工简要分析

人们习惯于将消极怠工、只出工不出力的现象称为“磨洋工”。对公司而言,磨洋工自然不是好事情,对员工而言,磨洋工有时却是无可奈何的选择。

什么时候会出现磨洋工?当公司简单的以工作时间来统计工作量时,工作效率高的人自然就会磨洋工,否则做得越快,事情就越多,却又得不到相应的回报,所以老油条员工在领受任务时,都会有意延长完成任务的期限,降低上级的预期,从而以安全的平均速度完成任务,即不会因为落后被批评或开除,也不会给自己揽太多吃力不讨好的事情。

显然,以实际的工作量来衡量员工的贡献更合理,可是工作量的统计标准又是相当成问题的,如果它太过主观,就变成了领导的一言堂,员工会开展办公室政治来争夺利益,如果它十分客观,那么员工会试图去理解评价标准,然后把自己的所有行为都适配到这个标准上,而标准是否与公司的综合利益完全一致有时是存在问题的,这就出现了刷KPI,刷评分而实际上损害公司利益的事情。

所以好的公司还是要有价值观的感召力,如果仅仅是为了钱而工作,往往会损害公司的长远利益。但话又说回来,价值观感召力是建立在基本的按劳分配机制上的,只能是一个补充机制,如果只谈理想不谈钱,那就是耍流氓,脑子没缺根筋的人是不会用自己的情怀来为别人的奢华买单的。

对员工而言,磨洋工是无奈的反抗,它最大的危险是降低自己进步的速度,在消极中泯灭了人生的激情,万一你不得不磨洋工,那么最好是能做一件事,那就是为跳槽做准备。