C#开发笔记概述

该文章的最新版本已迁移至个人博客【比特飞】,单击链接 https://www.byteflying.com/archives/962 访问。

从A函数跳转到B函数,在B函数执行完毕后,程序为什么能精确的返回到A函数中未执行完的代码区域?

首先,我们要知道什么是栈和栈帧。

栈是一种特殊的线性表,仅能在线性表的一端-栈顶进行操作,栈底不允许操作。

栈的特性:后进先出(Last In First Out or LIFO)

栈帧是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等。

为了我们不会因为文字的描述而产生歧义,我们约定程序从左向右运行,左边为前,右边为后。

从函数A跳转到函数B时,将函数的执行环境压入栈中,显然栈帧就是栈中所需要存放的数据。函数B执行完成后,运行环境知道,刚执行完成了一个函数,现在要回到某个地方了,那么回到哪里呢?当然是从栈中弹出最近一个栈帧(如果有的话)并取出相关信息即可。

这堪称是一个完美的设计!这种设计为函数式编程带来了高可靠性的同时也带来了性能损失。事实上运行环境维护这样的数据结构并不轻松,首先我们需要一个特殊的数据区域来存放这个栈,这个栈通常被称为“调用堆栈”(Call Stack),并且通常有 1M 的空间限制(注意这个值是可以改的)和  次的数量限制(32位系统,一般情况下 1M 的空间限制首先到达)。即每次从一个函数跳转到另外一个函数会使栈增加一个计数并存放栈帧信息,下次程序执行路径在函数终点处需要返回时又要从栈中取出栈顶信息(如果有的话)。但情况并非总是如此!

一般情况下总是需要这样的一个栈帧,但如果函数A跳转到函数B时,该处已经是函数A的最后一句是又会怎么样呢?显然,这个栈帧不是必须的,因为返回此处时,由于函数A也即将结束,又会往前返回到上一个栈帧,那何不直接返回到上一个栈帧呢?事实上现代编译器都会为这种情况做出优化,运行时不会为其增加栈帧,而是在函数B运行完成后,直接返回至函数A之前所存放的栈帧信息(如果有的话)。显然递归属于特殊函数的跳转,它跳转到其本身。

再来看看什么是尾递归。

如果一个函数中所有递归形式的调用都出现在函数的末尾(逻辑上的末尾或者说代码路径的末尾),我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。

我们通过1个案例来具体的分析一下,之后再回头看上面的描述可能会更加清楚的了解运行环境是如何追踪函数的运行状态的。

public class Program {

    public static void Main(string[] args) {
Add(100);
Add(95);
Add(80);
Console.ReadKey();
} private static void Add(int score) {
#line 100
if(score == 100) {
Perfect();
}
//请注意这里是另起if
#line 200
if(score >= 90) {
Excellent();
}
#line 300
else {
ComeOn();
}
} private static void Perfect() {
Console.WriteLine("Perfect!");
} private static void Excellent() {
Console.WriteLine("Excellent!");
} private static void ComeOn() {
Console.WriteLine("ComeOn!");
} }

我们先来分析 Add(100) 的执行,Main->Add->Perfect,每次跳转函数都会增加一个栈帧,以便程序执行可以沿着 Perfect->Add->Main 这样的路径往前返回。在这个过程中显然每次函数的跳转都会增加栈帧。

如果你已经明白了 Add(100) 的执行过程,那么现在要分析的 Add(90) 的执行过程可能不会太难。但需要注意到的一点是,当分数为90的时候,第100行的代码(#line 100)被执行到的时候,运行环境已经知道这是最后一行可以被执行到的代码,因为 if else 中只有一个可以被命中的路径,所以程序往前返回时的路径是这样的 Excellent->Add->Main (红色删除部分表示未被返回) 。因为 Add(90) 跳转到 Excellent 方法时,没有为其增加栈帧,因为完全没有必要。显然 Add(80) 也是这样的。

接一下,我们再看看函数有返回值时,会出现什么情况。

private static void Add(int score) {
string description = string.Empty;
#line 100
if(score == 100) {
description = Perfect();
}
//请注意这里是另起if
#line 200
if(score >= 90) {
description = Excellent();
}
#line 300
else {
description = ComeOn();
}
} private static string Perfect() {
return "Perfect!";
} private static string Excellent() {
return "Excellent!";
} private static string ComeOn() {
return "ComeOn!";
}

在分析这种情况前,我们再来回顾一下尾递归的概念:

该文章的最新版本已迁移至个人博客【比特飞】,单击链接 https://www.byteflying.com/archives/962 访问。

如果一个函数中所有递归形式的调用都出现在函数的末尾(逻辑上的末尾或者说代码路径的末尾),我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。

显然,当需要这个函数的返回值时,其不属于尾递归。

C#开发笔记之06-为什么要尽可能的使用尾递归,编译器会为它做优化吗?的更多相关文章

  1. C#开发笔记,点点细微,处处真情,记录开发中的难言之隐

    该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/956 访问. 概述 本系列文章将会向大家介绍本人实际开发过程中所遇到技术 ...

  2. 驱动开发学习笔记. 0.06 嵌入式linux视频开发之预备知识

    驱动开发读书笔记. 0.06  嵌入式linux视频开发之预备知识 由于毕业设计选择了嵌入式linux视频开发相关的项目,于是找了相关的资料,下面是一下预备知识 UVC : UVC,全称为:USB v ...

  3. TERSUS无代码开发(笔记06)-简单实例手机端页面设计

    手机端的设计 1.页面说明 2.默认页面===>提交请假单(上面页面双击进入,页面主要编辑区) 2.1默认页面===>提交请假单===>头部区(页面部份主要编辑区01) 2.1.1默 ...

  4. cocos2dx3.0 超级马里奥开发笔记(两)——正确的规划游戏逻辑

    我将不得不拿出一个完整的开发笔记.由于个人原因.代码已OK该,博客,那么就不要粘贴代码,直接解释了整个游戏设计,更确切地说,当新手应该注意的地方发展. 1.继承类和扩展作用的权----展阅读(MVC) ...

  5. iOS回顾笔记(06) -- AutoLayout从入门到精通

    iOS回顾笔记(06) -- AutoLayout从入门到精通 随着iOS设备屏幕尺寸的增多,当下无论是纯代码开发还是Xib/StoryBoard开发,自动布局已经是必备的开发技能了. 我使用自动布局 ...

  6. Java开发笔记(四十)日期与字符串的互相转换

    前面介绍了如何通过Date工具获取各个时间数值,但是用户更喜欢形如“2018-11-24 23:04:18”这种结构清晰.简洁明了的字符串,而非啰里八唆依次汇报每个时间单位及其数值的描述.既然日期时间 ...

  7. 安卓开发笔记——打造万能适配器(Adapter)

    为什么要打造万能适配器? 在安卓开发中,用到ListView和GridView的地方实在是太多了,系统默认给我们提供的适配器(ArrayAdapter,SimpleAdapter)经常不能满足我们的需 ...

  8. 【转】Android开发笔记——圆角和边框们

    原文地址:http://blog.xianqu.org/2012/04/android-borders-and-radius-corners/ Android开发笔记——圆角和边框们 在做Androi ...

  9. Java开发笔记(一百零一)通过加解锁避免资源冲突

    前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...

随机推荐

  1. Spring Boot 2.x基础教程:使用EhCache缓存集群

    上一篇我们介绍了在Spring Boot中整合EhCache的方法.既然用了ehcache,我们自然要说说它的一些高级功能,不然我们用默认的ConcurrentHashMap就好了.本篇不具体介绍Eh ...

  2. 01 . Git常用命令及方法和分支管理

    原理 # Workspace:工作区 # Index / Stage:暂存区 # Repository:仓库区(或本地仓库) # Remote:远程仓库 本地分支关联远程 git branch --s ...

  3. ES模糊查询来对应mysql的like查询

    使用ES查询来对应mysql的like查询 建立一个测试索引 PUT /test_like1 { "mappings" : { "properties" : { ...

  4. MySQL(四)数据备份与还原

    数据备份与还原: 备份:将当前已有的数据或者记录保留 还原:将已经保留的数据恢复到对应的表中 为什么要做备份还原: 1.防止数据丢失:被盗.误操作 2.保护数据的记录 数据备份还原的方式很多:数据表备 ...

  5. VIM的常用快捷方式(尽量简洁,删去能组合实现的且不易记的)

    vi可以分为三种状态,分别是一般模式.编辑模式和命令行模式 1一般模式:以vi打开一个文件就直接进入一般模式了(这是默认的模式).在这个模式中, 你可以使用上下左右按键来移动光标,你可以使用删除字符或 ...

  6. IO—》字节流&字符流

    字节流 一.字节输出流OutputStream OutputStream此抽象类,是表示输出字节流的所有类的超类.操作的数据都是字节,定义了输出字节流的基本共性功能方法. FileOutputStre ...

  7. MySQL乱码问题(为什么?追根溯源)

    引言 定位:查询数据库得到的结果集乱码,client端向数据库插入数据乱码. 网上有不少帖子,手把手地教给我们如何去改这一问题.方案大多数如下: 方案中最多介绍的就是更改配置文件,win下my.ini ...

  8. pycharm 退出虚拟环境

    pycharm 内置虚拟环境 venv 如果要退出就直接 deactivate 命令就行 运行的话直接在命令行输python3  文件名

  9. ROS 机器人技术 - 广播与接收 TF 坐标

    上次我们学习了 TF 的基本概念和如何发布静态的 TF 坐标: ROS 机器人技术 - TF 坐标系统基本概念 ROS 机器人技术 - 静态 TF 坐标帧 这次来总结下如何发布一个自定义的 TF 坐标 ...

  10. Mybatis-Plus中Wrapper的方法

    public interface EntityService extends IService<TbEntity>{ }entityService.update(entity,Condit ...