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. 测试工程师想进BAT必须具备的几项素质

    我发现一个奇怪的现象:总是听到身边的程序员朋友谈论BAT(中国大陆互联网的三大巨头:百度.阿里.腾讯)以及如何进入BAT,却鲜少有测试会去谈论或者考虑这些问题. 我不知道这是为什么,或者我就算知道也只 ...

  2. Python Hacking Tools - Web Scraper

    Preparation: Python Libray in the following programming: 1. Requests Document: https://2.python-requ ...

  3. 牛客练习赛 66B题解

    前言 当初思路 开始没想到异或这么多的性质,于是认为对于每个点\(u\),可以和它连边的点\(v\)的点权 \(a_v=a_u \oplus k\)(证明:\(\because\) \(a_u\opl ...

  4. C++语法小记---类模板

    类模板 类模板和函数模板类似,主要用于定义容器类 类模板可以偏特化,也可以全特化,使用的优先级和函数模板相同 类模板不能隐式推倒,只能显式调用 工程建议: 模板的声明和实现都在头文件中 成员函数的实现 ...

  5. python 简单粗暴的生产的验证码

    import os import pygame import random from pygame.locals import * count = 0; 生成验证码的函姝 def get_code() ...

  6. Nginx配置多个域名指向不同的端口

    一.前言 很多时候我们都会在同一个服务器上部署多个项目,我们有多个域名的情况下,怎么样使不同的域名都通过80端口来访问呢?打个比方说,现在有2个域名,分别是:tessai.cn 和 admin.tes ...

  7. Flutter学习笔记(41)--自定义Dialog实现版本更新弹窗

    如需转载,请注明出处:Flutter学习笔记(41)--自定义Dialog实现版本更新弹窗 功能点: 1.更新弹窗UI 2.强更与非强更且别控制 3.屏蔽物理返回键(因为强更的时候点击返回键,弹窗会消 ...

  8. 一文了解JDK12 13 14 GC调优秘籍-附PDF下载

    目录 简介 那些好用的VM参数 G1的变化 配置FlightRecorder RAM参数 JDK13中的ZGC RTM支持 总结 简介 想了解JDK12,13,14中的GC调优秘籍吗?想知道这三个版本 ...

  9. 阿里云ecs轻量级服务器node镜像部署

    这个是自带安装pm2,nginx,node,mongodb的环境的,目录在控制台有给出, server端的配置按照开发手册去操作即可. 而静态的页面.需要修改nginx的配置文件,找到nginx的ng ...

  10. 在Linux系统中使用Vim读写远程文件

    大家好,我是良许. 今天我们讨论一个 Vim 使用技巧--用 Vim 读写远程文件.要实现这个目的,我们需要使用到一个叫 netrw.vim 的插件.从 Vim 7.x 开始,netrw.vim 就被 ...