<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Nicholas Zhan - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Nicholas Zhan - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 13 May 2026 08:54:08 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/nicholas/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何阅读研究论文：设定研究目标、寻找论文阅读，不止于此 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你从事某一科学领域，则应尝试对该领域建立起深刻而公正的理解。这不仅是在以尽可能最好的方式教育你，还在帮你预见本领域中的机会。 研究论文通常是围绕某个主题的广泛实践，深入而真实。在撰写研究论文时，作者批判性地思考问题，进行严谨的研究，评估研究过程与信息来源，组织想法，然后开写。这些真正地动手实践最终产生出一篇好的研究论文。 如果你想养成定期阅读论文的习惯（像我一样），我已经试着分解了整个过程。我已经与该领域中的研究者进行了交谈，阅读了大量杰出研究人员的论文和博客，并记录下了一些可以供你遵循的技巧。 让我们从理解什么是研究论文，研究论文又不是什么开始吧！ 什么是研究论文 研究论文是密集而详尽的手稿，汇编着对某一问题或主题的透彻理解。它提出解决方案以及进一步的研究，还有本研究演绎和进行的条件、解决方案的效力和所进行的研究、以及研究中潜在的漏洞。 撰写研究论文不仅可以提供难得的学习机会，还可以为该领域的进一步发展铺平道路。这些论文帮助其他学者萌生思想的种子，这些种子要么带来一个新的思想世界，要么激发出一个解决某个长期问题的创新方法。 研究论文不是什么 人们普遍认为，研究论文是关 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/building-a-habit-of-reading-research-papers/</link>
                <guid isPermaLink="false">604e1dd46ce45b059394b862</guid>
                
                    <category>
                        <![CDATA[ 研究 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Mon, 14 Jun 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/03/October-AI.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>如果你从事某一科学领域，则应尝试对该领域建立起深刻而公正的理解。这不仅是在以尽可能最好的方式教育你，还在帮你预见本领域中的机会。</p>
<p>研究论文通常是围绕某个主题的广泛实践，深入而真实。在撰写研究论文时，作者批判性地思考问题，进行严谨的研究，评估研究过程与信息来源，组织想法，然后开写。这些真正地动手实践最终产生出一篇好的研究论文。</p>
<p>如果你想养成定期阅读论文的习惯（像我一样），我已经试着分解了整个过程。我已经与该领域中的研究者进行了交谈，阅读了大量杰出研究人员的论文和博客，并记录下了一些可以供你遵循的技巧。</p>
<p>让我们从理解什么是研究论文，研究论文又不是什么开始吧！</p>
<h2 id="">什么是研究论文</h2>
<p>研究论文是密集而详尽的手稿，汇编着对某一问题或主题的透彻理解。它提出解决方案以及进一步的研究，还有本研究演绎和进行的条件、解决方案的效力和所进行的研究、以及研究中潜在的漏洞。</p>
<p>撰写研究论文不仅可以提供难得的学习机会，还可以为该领域的进一步发展铺平道路。这些论文帮助其他学者萌生思想的种子，这些种子要么带来一个新的思想世界，要么激发出一个解决某个长期问题的创新方法。</p>
<h2 id="">研究论文不是什么</h2>
<p>人们普遍认为，研究论文是关于某一问题或主题的翔实的总结，由其它来源写成。</p>
<p>但是，你不应该把它误认为是一本书或某人在特定主题上发表的自以为是的见解。</p>
<h2 id="">为什么你应该阅读研究论文</h2>
<p>我发现阅读优质研究论文让人着迷之处在于，你可以对某个主题进行深入研究并以新的观点与社区互动，从而了解该主题及其周边可以实现的目标。</p>
<p>我从事教学设计与数据科学的交叉领域。学习是我日常工作的一部分。如果我的教育资源有瑕疵或效率底下，那么从长期来看，我的工作会失败。这适用于科学领域的很多其它工作，尤其是研究工作。</p>
<p>有三个阅读研究论文的重要理由：</p>
<ol>
<li><strong>知识</strong>——从别人的视角理解问题，他们可能花了数年时间解决它，并且注意到了你在一开始可能不会考虑到的边界情况。</li>
<li><strong>探索</strong>——无论你是否拥有一个明确的日程，都有很大的机会发现一个值得跟进的边界情况或缺陷。通过长时间的不懈努力，你可以学会利用这些知识谋生。</li>
<li><strong>研究与审查</strong>——写研究论文的一个主要原因是促进该领域的发展。研究人员阅读论文，为会议评审它们，或对一个新的领域进行文献调查。例如，<a href="http://yann.lecun.com/exdb/publis/pdf/lecun-89e.pdf">Yann Lecun</a> 关于将域约束集成到反向传播中的论文在 1989 年为现代计算机视觉奠定了基础。经过数十年的研究与开发工作，我们目前已经完成了诸如对象检测和自动驾驶汽车优化之类的问题。</li>
</ol>
<p>不仅如此，在因特网的帮助下，你可以用所有这些原因或好处来推测多种业务模型。它可以是一个技术先进的创新型产品、一个高效的服务模型、一个内容创建者，也可以是一份理想的工作，你在这份工作中解决对你来说很重要的问题。</p>
<h2 id="">阅读研究论文的目标——你应该阅读哪些？</h2>
<p>首先要做的就是搞清楚你阅读论文的动机。有两种可能让你阅读论文主要场景：</p>
<ol>
<li><strong>场景 1</strong>——你有一个清晰的日程/目标，并且你已经融入了某个特定的领域。例如，你是一名 NLP 从业者并且你想学习 GPT-4 是如何让我们在 NLP 领域取得突破的。这始终是一个不错的场景，因为它提供了清晰的信息。</li>
<li><strong>场景 2</strong>——你想跟上某些领域的发展，比如<a href="https://deepmind.com/blog/article/alphafold-a-solution-to-a-50-year-old-grand-challenge-in-biology">一个新的深度学习架构是如何帮助我们解决了一个已经存在了 50 年的理解蛋白质结构的生物学问题</a>。对于初学者或那些每天花时间从研究论文中获取新闻的人来说（是的，他们是存在的！），这很常见。</li>
</ol>
<p>如果你是一位好奇的初学者，但又不知从何开始，请从场景 2 开始。列出几个你想要阅读的主题，直到找到让你感到好奇的领域。这最终会把你带到场景 1。</p>
<h3 id="ml">ML 重现挑战</h3>
<p>除了这些通用目标外，如果你的最终目标是养成阅读研究论文习惯，你应该查看 <a href="https://openreview.net/group?id=ML_Reproducibility_Challenge/2020">ML 重现挑战</a>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1.png" alt="1" width="600" height="400" loading="lazy"></p>
<p><a href="https://openreview.net/group?id=ML_Reproducibility_Challenge/2020">https://openreview.net/group?id=ML_Reproducibility_Challenge/2020</a></p>
<p>你将从世界级会议上找到值得深入并重现结果的顶级论文。</p>
<p>他们一年举行两场这个挑战，在 <a href="https://paperswithcode.com/rc2020">2021 年春季</a>就有一场。你应该研究过去三个版本的挑战，我会写一篇详细的文章，介绍应该期望什么、如何准备，等等。</p>
<p><strong>现在你肯定想知道——如何能找到正确的论文来读？</strong></p>
<h2 id="">如何找到正确的论文来读</h2>
<p>为了获取一些相关想法，我向我的朋友 <a href="https://scholar.google.com/citations?user=zd0-SNQAAAAJ&amp;hl=en&amp;oi=ao">Anurag Ghosh</a> 求助，他是一名微软的研究员。Anurag 一直致力于计算机视觉、机器学习和系统工程的交叉研究。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-04-at-12.08.31-AM.png" alt="Screenshot-2021-03-04-at-12.08.31-AM" width="600" height="400" loading="lazy"></p>
<p><a href="https://anuragxel.github.io/">https://anuragxel.github.io/</a></p>
<p>这里是他的一些入门技巧：</p>
<ul>
<li>始终选择一个你感兴趣的领域。</li>
<li>阅读几本有关该主题的好书或者几篇详细的博客文章，然后通过阅读这些资源中参考的论文进行深入。</li>
<li>寻找围绕该主题的开创性论文。这些论文报告该领域的重大突破，并提供了新的方法观点，这些观点蕴藏着在该领域中进行后续研究的巨大潜力。查看 <a href="https://blog.acolyer.org/">the morning paper</a> 或 <a href="https://www.thecvf.com/?page_id=413#Helmholtz">CVF</a>——时间检验奖/赫姆霍兹奖（如果你对计算机视觉感兴趣的话）。</li>
<li>查看像 Richard Szeliski 的 <strong>《计算机视觉：算法与应用》</strong> 这样的书，并寻找书中参考的论文。</li>
<li>拥有并建立社区意识。找到志趣相投的人，加入促进这些活动的 group/subreddits/discord 频道。</li>
</ul>
<p>除了这些宝贵的技巧之外，在搜寻正确的论文来读的时候，还有很多网络应用帮助我缩小了搜索范围，我已经将它们列出来了：</p>
<ul>
<li><a href="https://www.reddit.com/r/MachineLearning/">r/MachineLearning</a>——有很多研究员、从业者和工程师在这里分享他们的工作成果以及在取得这些成果的过程中发现的有用的论文。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-01-at-10.55.53-PM.png" alt="Screenshot-2021-03-01-at-10.55.53-PM" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.reddit.com/r/MachineLearning/">https://www.reddit.com/r/MachineLearning/</a></p>
<ul>
<li><a href="http://www.arxiv-sanity.com/top">Arxiv Sanity Preserver</a>——由 Andrej Karpathy 建立，旨在加快研究速度。它是一个有着 142846 篇论文的仓库，涵盖计算机科学、机器学习、系统、人工智能、统计、计算机视觉，等等。它还提供了很多过滤器、强大的搜索功能，以及一个论坛，造就了一个超级有用的研究平台。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-01-at-10.59.41-PM.png" alt="Screenshot-2021-03-01-at-10.59.41-PM" width="600" height="400" loading="lazy"></p>
<ul>
<li><a href="https://research.google/">Google Research</a>——这个谷歌的研究团队正对那些影响着我们日常生活的问题进行研究。他们分享自己的出版物，供个人和团队学习、贡献并加速相关研究。他们也有一个谷歌 AI 博客，你可以看看。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-01-at-11.13.31-PM.png" alt="Screenshot-2021-03-01-at-11.13.31-PM" width="600" height="400" loading="lazy"></p>
<h2 id="">如何阅读研究论文</h2>
<p>在你准备好阅读清单之后，接下来便是阅读这些论文的过程。记住，并不是每一篇论文都有用，我们需要一种可以帮助我们快速筛选值得阅读的论文的机制。</p>
<p>为了应对这一挑战，你可以使用 <strong><a href="http://ccr.sigcomm.org/online/files/p83-keshavA.pdf">S. Keshav 的三步阅读法</a></strong> 。该方法建议你分三个步骤阅读论文，而不是从头开始，一路深入直到结束。</p>
<h3 id="">三步阅读法</h3>
<ol>
<li><strong>第一步</strong>——快速扫描，捕获该论文的宏观观点。仔细阅读论文的题目、摘要和简介，接着阅读各章节与子章节的标题，最后阅读总结部分。应该要不了 5-10 分钟，你就能知道你是否想进入第二步了。</li>
<li><strong>第二步</strong>——仔细阅读，但不用检查技术证明。写下所有重要的笔记，在页面空白处突出关键点。仔细研究图形、图表和插图。回顾其中的图片，标记出未阅读的相关参考以便进一步阅读。这能帮助你理解该论文的背景。</li>
<li><strong>第三步</strong>——到了这一步就说明你已经发现了一篇你想要深入理解或审阅的论文。第三步的关键在于重现论文中的结果。检查所有的假设，并记下重新实现过程中所有的变化和原始结果。写下所有的想法，便于将来的分析。对初学者来说，这一步应该会花费 5-6 小时，而有经验的读者只需要 1-2 小时。</li>
</ol>
<h2 id="">跟踪论文流水线的工具和软件</h2>
<p>如果你真心实意地阅读研究论文，你的论文列表很快就会变成一个难于跟踪的庞然大物。幸运的是，我们有软件，可以帮助我们建立一种机制来管理我们的研究。</p>
<p>这里是一些你可以使用的软件：</p>
<ul>
<li><a href="https://www.mendeley.com/?interaction_required=true"><strong>Mendeley</strong></a> <strong>【不免费】</strong>——你可以从浏览器直接添加论文到你的文献库中，导入文件，生成参考文献和引文，与研究者协作，从任何地方访问你的文献库。这主要是由经验丰富的研究人员使用。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-02-at-1.28.19-AM.png" alt="Screenshot-2021-03-02-at-1.28.19-AM" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.mendeley.com/?interaction_required=true">https://www.mendeley.com/?interaction_required=true</a></p>
<ul>
<li><strong>Zotero 【免费 &amp; 开源】</strong>——与 Mendeley 路线相同，但是它是免费的。你可以在有限的存储空间内利用所有的功能特性。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-02-at-1.42.28-AM.png" alt="Screenshot-2021-03-02-at-1.42.28-AM" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.zotero.org/">https://www.zotero.org/</a></p>
<ul>
<li><strong>Notion</strong>——如果你刚开始并想使用可以在一个工作区中整理论文、记笔记并管理一切的轻量级软件，那么它非常适合你。它可能比不过上述工具，但是我个人在使用 Notion 时感觉很舒服，我还创建了<a href="https://www.notion.so/My-paper-pipeline-ec3ff02ce9c641d2953f6cdbc431a55a">这个面板</a>，用来跟踪我现在的进度，你可以复制它：</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/2.png" alt="2" width="600" height="400" loading="lazy"></p>
<h2 id="">⚠️ 阅读论文的症状</h2>
<p>阅读研究论文可能令人沮丧、备受挑战和耗时，尤其当你是初学者的时候。你可能会遇到以下常见的症状：</p>
<ul>
<li>你可能因不理解论文在说什么而开始觉得自己不聪明。</li>
<li>发现你自己很难理解那些证据背后的数学原理。</li>
<li>用头撞墙，让满脑子中都是论文中使用的首字母缩略词。只是开个玩笑啦，你将不得不时不时地查找那些缩略词。</li>
<li>在某个段落上卡住了超过一个小时。</li>
</ul>
<p>这里是一个完整清单，展示了你可能经历的各种表情，就像 Adam Ruben 在<a href="https://www.sciencemag.org/careers/2016/01/how-read-scientific-paper">这篇文章</a>中解释的一样。</p>
<h2 id="">关键要点</h2>
<p>我们应该全力以赴。这里是我们所介绍内容的一份简要概述：</p>
<ul>
<li>科研论文是一项深度研究，它为某一主题或问题提供详尽的解释，还包含研究过程、证据、解释的结果和关于未来工作的想法。</li>
<li>阅读研究论文，从而形成对某一主题/问题的深刻理解。然后你既可以作为研究员的一部分审阅论文，探索该领域和该类问题，从而构建解决方案或者就从此开始研究，还可以简单地通过阅读它们了解兴趣领域的最新发展。</li>
<li>如果你是一名初学者，从探索开始，尽快找出通往目标研究的道路。</li>
<li>为了找到好的论文来读，你可以使用像 arxiv sannity、google research 这样的网站，也可以使用像 r/MachineLearning 这样的小社区（subreddits）。</li>
<li>阅读方法——使用三步方法寻找论文。</li>
<li>使用 Zotero/Notion 这类工具跟踪你的研究、笔记和进展。</li>
<li>阅读论文可以立马让你不堪重负。确保你懂简单开始，并逐步增加负载。</li>
</ul>
<p><strong>记住：艺术并不是在一个周末就可以完成的简单方法或步骤，而是一个随时间推移而取得显著成果的过程。</strong></p>
<p>你也可以在我的 <a href="https://www.youtube.com/channel/UCH-xwLTKQaABNs2QmGxK2bQ">YouTube 频道</a> 上观看这一主题的视频：</p>
<p><a href="https://www.youtube.com/channel/UCH-xwLTKQaABNs2QmGxK2bQ">https://www.youtube.com/channel/UCH-xwLTKQaABNs2QmGxK2bQ</a></p>
<p>如果你有一些点子、问题或想法，请随时响应这篇博客或者在视频上发表评论！</p>
<p>你可以通过 <a href="https://twitter.com/tyagi_harshit24">Twitter</a> 或 <a href="https://harshit-tyagi.medium.com/">LinkedIn</a> 联系我。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/building-a-habit-of-reading-research-papers/">How to Read a Research Paper – A Guide to Setting Research Goals, Finding Papers to Read, and More</a>，作者：<a href="https://www.freecodecamp.org/news/author/harshit/">Harshit Tyagi</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 开源项目负责人指南 ]]>
                </title>
                <description>
                    <![CDATA[ 你有没有想过拥有自己的开源项目？我敢打赌，你一定有——因为你正在读这篇文章。 也许你现在正在考虑这事儿。也许你来这里是为了了解应该对开源项目有何期望，你将面临哪些挑战，以及如何应对这些挑战。好吧，你来对地方了。 以下指南基于我个人拥有一个开源项目中的经验。我指的是拥有一个开源项目，而不仅是向一个开源项目做贡献。这两者之间有着巨大的差别，我们将学习为什么。 所以让我们从开源项目负责人终极指南开始吧…… 目录  * 介绍  * 什么是开源  * 为什么要启动一个开源项目  * 如何发起一个开源项目  * 如何编写文档  * 如何推广你的开源项目  * 如何管理议题与拉取请求  * 如何实现流程自动化  * 版本管理 我是谁 我叫 Jeb，这几年一直在维护几个开源项目。其中最受欢迎的一个是 @angular-builders [https://github.com/just-jeb/angular-builders]，它也是我从中学到最多东西的一个开源项目。在写这篇文章的时候，它在 GitHub 上有约九百颗星，月下载量大约一百万。 是的，它与 Angular 或 React  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/ultimate-owners-guide-to-open-source/</link>
                <guid isPermaLink="false">6072e446b3b14e058ee04b6d</guid>
                
                    <category>
                        <![CDATA[ 开源 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Sat, 10 Apr 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/mark-konig-fbKMKNVJjwo-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>你有没有想过拥有自己的开源项目？我敢打赌，你一定有——因为你正在读这篇文章。</p>
<p>也许你现在正在考虑这事儿。也许你来这里是为了了解应该对开源项目有何期望，你将面临哪些挑战，以及如何应对这些挑战。好吧，你来对地方了。</p>
<p>以下指南基于我个人<strong>拥有</strong>一个开源项目中的经验。我指的是拥有一个开源项目，而不仅是向一个开源项目做贡献。这两者之间有着巨大的差别，我们将学习为什么。</p>
<h3 id="">所以让我们从开源项目负责人终极指南开始吧……</h3>
<h2 id="">目录</h2>
<ul>
<li><a href="#%E6%88%91%E6%98%AF%E8%B0%81">介绍</a></li>
<li><a href="#%E9%82%A3%E4%B9%88%E4%BB%80%E4%B9%88%E6%98%AF%E5%BC%80%E6%BA%90">什么是开源</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%90%AF%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE">为什么要启动一个开源项目</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E5%8F%91%E8%B5%B7%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE">如何发起一个开源项目</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E4%B8%BA%E4%BD%A0%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E7%BC%96%E5%86%99%E6%96%87%E6%A1%A3">如何编写文档</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E6%8E%A8%E5%B9%BF%E4%BD%A0%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE">如何推广你的开源项目</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E7%AE%A1%E7%90%86%E8%AE%AE%E9%A2%98%E4%B8%8E%E6%8B%89%E5%8F%96%E8%AF%B7%E6%B1%82">如何管理议题与拉取请求</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E6%B5%81%E7%A8%8B%E8%87%AA%E5%8A%A8%E5%8C%96">如何实现流程自动化</a></li>
<li><a href="#%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86">版本管理</a></li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_now0_4liLR7fJcvvnWWStQ.jpeg" alt="1_now0_4liLR7fJcvvnWWStQ" width="600" height="400" loading="lazy"></p>
<h2 id="">我是谁</h2>
<p>我叫 Jeb，这几年一直在维护几个开源项目。其中最受欢迎的一个是 <a href="https://github.com/just-jeb/angular-builders">@angular-builders</a>，它也是我从中学到最多东西的一个开源项目。在写这篇文章的时候，它在 GitHub 上有约九百颗星，月下载量大约一百万。</p>
<p>是的，它与 Angular 或 React 这样的大型项目相差甚远，但是我觉得我有足够的经验和你分享，帮助你避免重蹈覆辙。更重要的是，帮助你了解拥有一个开源项目的成本。</p>
<h2 id="">那么什么是开源</h2>
<p>首先，让我们建立共同语言，并在关键术语和定义上达成共识。</p>
<p>什么开源（open source）？</p>
<p>这里是一个非常通用的<a href="https://www.wikiwand.com/en/Open_source_(disambiguation)">定义</a>，来自著名的开源百科全书（亦称维基百科）：</p>
<blockquote>
<p><a href="https://en.wikipedia.org/wiki/Open_source">开源</a>是指允许复制或修改的信息向公众开放的概念。</p>
</blockquote>
<p>或者，从<a href="https://en.wikipedia.org/wiki/Open-source_model">软件开发</a>模式来看：</p>
<blockquote>
<p>开源模式是一种去中心化的<a href="https://en.wikipedia.org/wiki/Software_development">软件开发</a>模式，鼓励<a href="https://en.wikipedia.org/wiki/Open_collaboration">开放协作</a>。</p>
<p><a href="https://en.wikipedia.org/wiki/Open-source_software_development">开源软件开发</a>的一个主要原则是<a href="https://en.wikipedia.org/wiki/Peer_production">对等生产</a>，源代码、<a href="https://en.wikipedia.org/wiki/Blueprint">蓝图</a>和文档等产品免费向公众开放。</p>
</blockquote>
<p>以维基百科为例，我们有编辑文章的人（贡献者），也有批准编辑的人（经验更加丰富的成员、版主）。</p>
<p>如果我们将其投射到软件世界，那些编辑们将形成一个开源项目的核心团队，而做贡献的人就是贡献者。</p>
<p>维基百科是一个非常庞大的开源项目，但这一切都是从一个<a href="https://en.wikipedia.org/wiki/History_of_Wikipedia">小东西</a>开始的。维基百科诞生自新百科全书（Nepedia），它的创建是有原因的：</p>
<blockquote>
<p>尽管邮件列表里有很多感兴趣的编辑，还有一个全职主编 <a href="https://en.wikipedia.org/wiki/Larry_Sanger">Larry Sanger</a>（威尔士聘请的<a href="https://en.wikipedia.org/wiki/Philosophy">哲学</a>研究生）在场，但新百科全书的内容写作还是非常缓慢，第一年只写了 12 篇文章。</p>
</blockquote>
<p>所以第一个问题来了……</p>
<h3 id="">为什么你应该关注开源</h3>
<p>你可以想象，将某些东西开放给更多人的一个主要的原因是 <em>为了招募协作者（collaborator）</em>。</p>
<blockquote>
<p>Together we’re strong.<br>
(Zarya, 2016)</p>
</blockquote>
<p>在写这篇文章的时候，维基百科<a href="https://en.wikipedia.org/wiki/Wikipedia:Wikipedians">拥有</a> 37899499 个注册账户，其中有 134022 个账户正在积极编辑。</p>
<p>想想看…… <strong>134022 名活跃的协作者</strong>。噢，它<a href="https://en.wikipedia.org/wiki/Special:Statistics">还有六百万的内容页</a>！</p>
<p>如果新百科全书（维基百科的前身，Nupedia）没有转向开源，这个数字还会是这样吗？我对此表示高度怀疑。</p>
<p>在软件领域也没有什么不同。为了解决某个问题，你必须写代码。为了解决一个大问题，你必须写很多代码。而为了妥善解决这个问题，你必须写出高质量的代码，做出高质量的设计，等等。</p>
<p>所有的这些都需要资源。说实话，你可能还没有这些资源。毕竟，你需要交房租。</p>
<h2 id="">为什么要启动一个开源项目</h2>
<p>虽然招募协作者是一个合理的动力，但几乎没有人仅仅因为这个原因就启动一个新的开源项目。你的理由可能会有所不同，但是我们来聊聊最流行的理由吧。</p>
<h3 id="1">#1 你想解决一个没有免费解决方案的问题</h3>
<p>你遇到一个问题，但没有任何东西可以为你解决它（或者有，但是要花钱），所以你不得不自己解决它。你设法解决了这个问题，你对自己的工作感到非常兴奋，而且你认为其他人可以从中受益，所以你把这个项目开源了。</p>
<h3 id="2">#2 你想成为发起人</h3>
<p>你想成为一个开源项目的发起人，你想在你的简历上写上那漂亮的一行。你真自以为是（毕竟，我们都是人）。如果这是你的<em>主要理由</em>，那么我向你保证——读完本指南之后，你会重新考虑的，这么做可能很不值得。</p>
<h3 id="3">#3 你想比别人更好地解决一个问题</h3>
<p>你面临一个问题，有一个开源项目实际上为你解决了这个问题，但是它不够好（在你看来）或者它没有你需要的确切功能。</p>
<p>如果你仅仅因为这个就创建一个新的开源项目，那么你 <em>很有可能</em>处于第二种情况（自以为是）。让自己成为一个贡献者，然后为现有的项目创建一个 PR 吧。</p>
<p>如果那个现有的项目有不同的愿景，创建 PR 并不可行，那么你应该考虑通过在你的项目中重用它的功能来扩展它，或者 <a href="https://help.github.com/en/github/getting-started-with-github/fork-a-repo">复刻（fork）</a>它，这可能会让你以后省去很多头疼的事情。</p>
<h3 id="4">#4 你想通过创建一个开源项目来解决一个问题</h3>
<p>你面临一个问题，并且现在没有任何人为你解决过它。所以，你认为从一开始就以开源的方式解决问题是一个非常好的想法。</p>
<p>在我看来，并不是。</p>
<p>解决这个问题，确保它对你有用，然后进入第一种情况。</p>
<p>这是我最常发现人们创建新的开源项目的四个动力。但是在这篇指南中，我们将主要关注第一种情况。</p>
<p>原因很简单——我相信，如果你发起开源项目的<em>主要原因</em>不是热衷于分享或者贡献你所做的东西，那么这就不成立了。</p>
<p>在相当长的一段时间内，你帮助别人这一事实可能是你得到的唯一回报。如果这不是你要找的那种满足感，那么你或许应该就此打住，不要再浪费你的时间了。</p>
<p>另一个相当流行场景值得一提：有些公司将它们的部分代码开放给社区。例如 Angular（由 Google 维护）、React（由 Facebook 维护）、VSCode（由微软维护）等等。</p>
<p>它们的理由可能各有不同，但是赢得协作者和为社区做贡献肯定是少不了的。</p>
<p>虽然我不能否认这种做法的重要性，但是这种情况与其它的情况有很大不同，因为维护这些项目的雇员们会因自己的工作<strong>得到报酬</strong>。</p>
<p>如果你在一家考虑创建开源项目的公司工作，那么这里的大部分内容对你来说还是有意义的，但是动力可能有所不同。</p>
<h3 id="">那么你是否应该创建一个开源项目呢？</h3>
<p>如果要我用一句话对这部分进行总结的话，那就是：</p>
<blockquote>
<p>确保你的意图符合你的期望。</p>
</blockquote>
<p>要相信，你想拥有一个开源项目与你实际拥有一个开源项目并不相同，你将会这接下来的几章中看到。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_6OX0spWqVQZxG3ue5D_EBA.jpeg" alt="1_6OX0spWqVQZxG3ue5D_EBA" width="600" height="400" loading="lazy"></p>
<h2 id="">如何发起一个开源项目</h2>
<p>所以你现在处于第一种情况——你有某个问题的解决方案，并且你渴望与世界分享。让我们再强调一遍：</p>
<ol>
<li>它与你的自负无关</li>
<li>你不是希望从中获益</li>
<li>你真的想要帮助有着相同问题的其他人</li>
</ol>
<p>对于所有这些问题，如果你的答案都是肯定的，那么这里是一份为你准备的快速检查单，让你确保你做的是正确的事情：</p>
<ol>
<li>确保开源是正确的形式。如果它只是某个你想与世界分享的小东西，那么一篇博客文章可能就足够了。</li>
<li>仔细检查是否已经有类似的项目存在。或许你的解决方案对某个已有的开源项目来说是一个绝佳的 PR。</li>
</ol>
<h3 id="">为即将到来的事情做好准备</h3>
<p>正如我所提到的，拥有一个开源项目会带来很多挑战。</p>
<p>其中最突出的一点就是，它需要你投入大量时间。你为项目做的一切都需要时间，无论是写代码、管理议题（issues）、更新依赖、与人交谈、回答问题等等。</p>
<p>你每多投入一分钟到你的开源项目中，你本可以投入到你的家庭、爱好、健康和其它的一切中的时间就会少一分钟。</p>
<p>你能做的唯一一件能让这种情况变好的事情就是开始委派任务。当（或者我应该说“如果”）你有足够多的协作者时，你可以将部分责任外包给你信任的人。</p>
<h3 id="">代码分离</h3>
<p>所以我们开始了，你有一个针对特定问题的解决方案，并且你认为其他人可以从中受益。这个解决方案仍然集成在你的代码库中，你可能并不想让整个代码库开源（除非你真的想）。</p>
<p>首先你需要将这部分代码从代码库中的其它代码分离出来。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/idigomnotoya-refactoring.jpg" alt="idigomnotoya-refactoring" width="600" height="400" loading="lazy"></p>
<p>这最终会意味着将要开源的所有代码都会位于一个单独的目录中。</p>
<h3 id="">让代码变得通用</h3>
<p>确保新目录中的代码是通用的，同时也不局限于你那特定的问题。如果需要的话，写一个抽象层。</p>
<p>举个例子，我发起<a href="https://github.com/just-jeb/angular-builders">angular-builders</a> 的时候，有一个非常具体的需求（来自<a href="https://github.com/just-jeb/electron-angular-native">我其它的开源项目之一</a>），就是为 Angular 构建添加一个自定义原生模块加载器。</p>
<p>我本可以创建<strong>原生模块构建器</strong>，它只用于这个目的。然而，我意识到我可以以相对较低的代价创建一个更加通用的解决方案，来解决相似（但不相同！）问题。</p>
<p><a href="https://github.com/just-jeb/angular-builders/tree/master/packages/custom-webpack">custom-webpack</a> 构建器就是这样诞生的。</p>
<h3 id="">保持简单</h3>
<p>通用的方案会非常棒，但要注意别过头了。</p>
<p>过早优化（premature optimization）和过度泛化（over-generalization）是软件工程中两个非常著名的问题。你应该找到那个最佳的点，你的解决方案可以解决你的问题之外的问题，<em>但是不能解决世界上所有的问题</em>。</p>
<p>如果你建立了一个量表，其中你那特定问题的解决方案是 1 ，而世界上所有问题的解决方案是 100，那么你应该从 2 开始。</p>
<p><em>你的通用代码应该能比你的特定代码多解决几个问题。</em></p>
<h3 id=""><a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food">吃自己的狗粮</a></h3>
<p>坚持在你的代码库中使用这部分通用代码——这样可以确保你去除不必要的部分，只留下必需的。它可以确保你要开源的代码能够正常工作。</p>
<p><em>记住，你是你的开源项目的第一个用户。</em></p>
<h3 id="">别被告了</h3>
<p>如果你正从公司的代码库中提取代码，咨询一下你的上级，如果需要的话，也咨询下法律部门。确保他们支持你的提议，并且你打算开源的那部分代码不会违反公司的知识产权。</p>
<p>这也能帮你决定哪个<a href="https://opensource.org/licenses">开源协议</a>更加适用于你的项目。</p>
<p>当一切正常，代码已经分离并且足够通用，你也得到了所有的批准（如果需要的话），那么就是时候向世界开放它了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_oT-ftfrBJ_BvmaZk2zumpg.jpeg" alt="1_oT-ftfrBJ_BvmaZk2zumpg" width="600" height="400" loading="lazy"></p>
<p>当你的开源代码分离完成并且通用化，就可以把它从你的代码库中完全分开了。</p>
<h3 id="">公开你的代码</h3>
<p>首先，你需要开放你项目的源代码（在一天结束时，这让它成为了一个开源项目！）。</p>
<p>在线托管源代码的<a href="https://stackify.com/source-code-repository-hosts/">选择有很多</a>，但是我们将会采用默认的——GitHub。</p>
<ol>
<li>在 GitHub 上<a href="https://help.github.com/en/github/getting-started-with-github/create-a-repo">创建一个新仓库</a></li>
<li>克隆该仓库</li>
<li>将源代码从你之前创建的目录（暂时不要删除这个目录）移动到这个仓库</li>
<li>提交并推送（Commit &amp; push）——就这样，它现在是一个开源项目了。</li>
</ol>
<p><em>不是吗？</em></p>
<h3 id="">创建一个包</h3>
<p>你的项目已公开，但是没有人使用它（包括你，因为你正在自己更大的代码库中使用这份代码的副本）。并且，没有人知道它的存在。</p>
<p>此外，你的项目在网上公开的唯一形式是<em>源代码</em>，而使用它的唯一方式就是将它复制-粘贴进代码库中。这种方式并不是很方便，对不对？</p>
<p>为了恰当地分发你的项目，你需要：</p>
<ol>
<li>从源代码创建一个包</li>
<li>将这个包放到某个公开的程序包注册中心中。（根据你的生态系统进行选择，例如，对 Java 来说可能是 <a href="https://search.maven.org/">Maven Central Repository</a>，对 JavaScript 来说可能是 <a href="https://www.npmjs.com/">Npm Package Registry</a>，等等）。</li>
</ol>
<p>这时，你要为你的新仓库添加一个构建链，确定项目的名字，等等。</p>
<p>我并不打算对整个过程进行分解，因为它非常依赖于你的生态系统、工具集和你使用的编程语言。</p>
<p>你可能无所不包，对你来说，确定新项目，添加构建链，发布程序包都是小菜一碟。如果是这样的话，那就好办了！</p>
<p>你可能习惯于只写代码，从未面对过所有的这些定义、配置、工件和类似的东西。对你来说，这可能是一个全新的新世界。</p>
<p>如果你是后者，就该学习了。我向你保证，要不了多久就能达成目标。</p>
<h3 id="">不论如何</h3>
<p>当你完成脑海中所有缺失拼图的拼装，你就学会了有关程序包注册中心的一切。并且你的程序包已经发布了，<em>那时，也只有那时你才能真正地认为你的项目开源了</em>。</p>
<p><em>这时，你可以告诉人们：“嘿，我已经有你那问题的解决方案了，你只需要把这个包添加到你的项目中，然后使用它就行了！”</em></p>
<h3 id="">进行完整性检查</h3>
<p>确保你的项目在开始像病毒一样传播之前能够正常工作。</p>
<p>对程序包的完整性检查，实际上就是从你那更大的代码库中删除通用的目录，并改用这个公开的程序包。</p>
<p>毕竟，<em>你是你的开源项目的第一个用户</em>。</p>
<h3 id="">如何处理你的代码库中的进一步开发问题</h3>
<p>当你开始在你的代码库中使用这个程序包时，开发流程可能会发生变化。在之前，开源代码是你代码库的一部分——你可以立即使用所做的更改。</p>
<p>但是现在，它与你代码中使用的任何其它第三方软件包一样，都是外部的软件包。</p>
<p>因此，当你在崭新的开源项目中开发新的功能时，必须先发布它，才能在你那更大的代码库中使用它。然而，如果你不确定它是否可行，你就不能发布它。因为一旦发布，就可能会影响其他用户。</p>
<p>为了避免发布有问题的版本，你可以做以下几件事情：</p>
<ol>
<li>用测试覆盖你的代码，包括单元测试和端到端测试。<br>
<em>我认为我不需要跟你讲测试是多么的重要。</em></li>
<li>在本地打包并安装新版本的程序包到你那更大的代码库中。<br>
<em>验证一切按预期工作之后，你就可以发布它了。</em></li>
<li>发布一个 Beta 版本，仅开放给那些明确希望使用该版本的用户，而不是全世界。<br>
<em>例如，在 npm 的程序包注册中心中，<a href="https://docs.npmjs.com/cli/dist-tag">dist tags</a> 就可以用于这个目的。</em><br>
<em>默认的 tag 是 <code>default</code>，当你运行 <code>npm install mypackage</code> 时，它实际上会运行 <code>npm install mypackage@latest</code>。当你用另一个 tag（比如 <code>beta</code>） 发布一个新版本时，人们只有显式地从这个 tag 安装才能获取到最新的版本：<code>npm install mypackage@beta</code>。</em></li>
</ol>
<h3 id="">小结</h3>
<p>与前一节的纯理论不同，这一节实际上会要求你做一些事情。根据你经验和学习能力，可能需要花费你几天甚至几周的时间来完成这个必需步骤，而我们甚至还没有开始呢。</p>
<p>这就是为什么我有责任再问你一次：</p>
<blockquote>
<p>你真的准备好将宝贵的时间奉献给社区了吗？</p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_z5sGJuWoz02x3uSBaF4tLg.jpeg" alt="1_z5sGJuWoz02x3uSBaF4tLg" width="600" height="400" loading="lazy"></p>
<h2 id="">如何为你的开源项目编写文档</h2>
<p>这篇文章的前两部分是针对那些正在考虑创建开源项目的人写的。我想让他们知道应该期望什么，并帮他们在开源世界里开个头。</p>
<p>这一部分，以及后面的部分，也会与那些已经在维护开源项目并希望有所改进的人们有关。</p>
<h3 id="">这一部分的基线：</h3>
<blockquote>
<p>你已经有了一个开源项目，我们可以在 GitHub 上访问它，并且可以很容易地通过某个程序包注册中心使用它。</p>
</blockquote>
<h3 id="">为什么你需要文档，它应该包含哪些内容？</h3>
<blockquote>
<p>一个没有文档的开源项目毫无生命可言</p>
</blockquote>
<p>之所以说毫无生命，是因为没有人会深入你的代码，去了解应该如何使用它。在此之前，甚至没有人知道你的代码是干嘛的。</p>
<p>所以你的文档应该基本上包含这两部分的内容——<em>它是做什么的</em>以及<em>如何使用它</em>。这两点是文档的奠基石，是文档的必备内容。</p>
<h3 id="">如何写项目描述</h3>
<p>人们在进入某个 GitHub 仓库时，首先看到的就是项目的描述信息。因此，一个好的描述应该简明扼要地回答“它是做什么的”这个问题。例如：</p>
<p><a href="https://github.com/facebook/react">React</a>:</p>
<blockquote>
<p><em>一个声明式的、高效且灵活的 JavaScript 库，用于构建用户界面。<a href="https://reactjs.org/">https://reactjs.org</a></em></p>
</blockquote>
<p><a href="https://github.com/moment/moment">Moment.js:</a></p>
<blockquote>
<p><em>在 JavaScript 中解析、验证、操作并展示日期。<a href="http://momentjs.com/">http://momentjs.com</a></em></p>
</blockquote>
<p><a href="https://github.com/just-jeb/angular-builders">Angular builders</a> （这个项目是我的）：</p>
<blockquote>
<p><em>Angular 构建门面扩展（Jest 与自定义 webpack 配置）</em></p>
</blockquote>
<p>你可以在仓库的 <code>About</code> 部分编辑描述信息：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screen-Shot-2021-03-11-at-10.20.43-1.png" alt="Screen-Shot-2021-03-11-at-10.20.43-1" width="600" height="400" loading="lazy"></p>
<h3 id="readmemd">如何写 README.MD 文件</h3>
<p>README.MD 是一个位于你的项目根目录中的文件，用 <a href="https://docs.github.com/cn/github/writing-on-github/basic-writing-and-formatting-syntax">Markdown 语法</a>编写，它包含别人需要知道的有关你的项目所需要的一切信息。</p>
<p>README 文件应该包含一个详细的描述（在“它是做什么的”这个问题上进行展开），以及关于“如何使用你的开源项目”的详细说明。</p>
<p>说明的内容应该覆盖每一个公共 API，最好是有使用示例。</p>
<p>这里编写良好 API 文档的几个要点：</p>
<ul>
<li><strong>最简法则</strong>——API 和示例越简单，使用者就越容易理解它是做什么的以及如何使用它。</li>
<li><strong>条理清晰</strong>——对所有的 API 方法都使用相同的模板和可视化结构。这样，你就可以定义自己的语言，向使用者传达 API。</li>
<li><strong>变身用户</strong>——总是从用户的角度编写 API 描述。假设你对项目的内部一无所知，并且这份文档就是你的全部。</li>
<li><strong>保持最新</strong>——随着项目的演进，API 可能会发生变化。确保你的 README 文件总是包含最新的 API 和示例。</li>
</ul>
<p>README 可以（但不是必须的）包含以下内容：</p>
<ul>
<li>贡献指南的链接</li>
<li>贡献者名单</li>
<li>变更日志的链接</li>
<li>最新版本</li>
<li>协议</li>
<li>构建状态</li>
<li>下载次数</li>
<li>用于快速反馈的聊天链接</li>
</ul>
<p><a href="https://github.com/aws-amplify/amplify-js">这里</a>是一个优秀 README 的示例。</p>
<h3 id="">何为徽标</h3>
<p>徽标（Badge）是一种很好的方式，可以直观地显示项目的基本信息，例如：构建状态、版本、协议以及项目使用的各种工具。</p>
<p>选择有很多，但是我推荐你使用 <a href="https://shields.io/">shields.io</a> 的徽标。他们的徽标很丰富。</p>
<p>给 README 文件添加徽标真的非常简单：</p>
<ol>
<li>前往 <a href="https://shields.io/">shields.io</a></li>
<li>选择合适的分类</li>
<li>点击你想要添加到 README 的徽标</li>
<li>填写需要的信息（如果需要的话）</li>
<li>从下拉菜单中选择 Copy Markdown</li>
<li>将 markdown 粘贴到你的 README 文件中</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-141342.png" alt="Screenshot-2021-03-12-141342" width="600" height="400" loading="lazy"></p>
<p>徽标通常放在 README 文件的顶部，就在详细描述的前面。它看起来像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_hgG8kurYMkdAsMXxji4iVg.png" alt="1_hgG8kurYMkdAsMXxji4iVg" width="600" height="400" loading="lazy"></p>
<h3 id="">确保你进行了测试</h3>
<p>API 参考很棒，但是没有什么比使用你的公共 API 的真实代码更好了。</p>
<p>完善文档的最佳方式之一就是用描述性测试来覆盖代码。有时，测试比任何文档更能解释代码。</p>
<h3 id="">小结</h3>
<p>在这一部分，我们只覆盖了文档的基础知识。例如，除了 README 和描述信息，还有很多其它内容。随着项目的发展和问题（issue）的出现，它们将成为文档的组成部分。</p>
<blockquote>
<p>然而，对于任何一个像样的开源项目来说，拥有一份覆盖公共 API 的 README 文件只是最低要求。</p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/david-menidrey-16ep3TGZR-0-unsplash.jpeg" alt="david-menidrey-16ep3TGZR-0-unsplash" width="600" height="400" loading="lazy"></p>
<h2 id="">如何推广你的开源项目</h2>
<p>我们已经讨论过发起一个项目意味着什么，如何以最佳的方式去做，以及如何为它写好的文档。</p>
<p>现在，我们就来聊聊如何将公众的注意力吸引到你的项目上来，以及如何在吸引与正确管理贡献者上面对项目进行优化。</p>
<h3 id="">这一部分的基线是：</h3>
<blockquote>
<p><em>你已经有了一个开源项目，人们可以在 GitHub 上访问它，它有良好的文档，并且可以很容易地通过某个程序包注册中心使用它。</em></p>
</blockquote>
<h3 id="">如何宣传你的项目</h3>
<p>咱们开门见山吧：随着项目的发展，你根本无法独自处理每件事情。所以，如果你想让项目长久生存下去，繁荣昌盛，你需要更多的人参与到项目中来。</p>
<p>为了让更多的人参与到你的项目中，你需要更多的人知道它，更重要的是相信它。</p>
<p>根据我的经验，将你的开源项目暴露给合适的受众的最佳方式是 <strong>使用知名的资源渠道</strong>，并写一篇关于项目的博客文章。</p>
<p>资源渠道可以是纯面向开发的（比如 dev.to），也可以不是（比如 Medium）。</p>
<p>所有这些资源之间都有一个共同点：它们都有既定的受众，并且是相关的受众。</p>
<p>你也可以在不同在线资源之间<a href="https://en.wikipedia.org/wiki/Crossposting">交叉发表</a>你的文章，从而覆盖到更到的受众。但是要注意，交叉发表有几个弊端：</p>
<ul>
<li>每个平台可能都有各自不同的标记语言，你不得不重新调整所有的格式</li>
<li>维护性——如果某部分内容变了（事情 <em>会</em> 发生变化），你就需要在所有的资源中对你的博客进行更新</li>
</ul>
<p>如果你选择 Medium，我会高度推荐你将自己的文章提交到某个<a href="https://getgist.com/top-50-medium-publications/">大型专栏</a>。这将要求你做更多的事情，因为你需要让你的文章满足专栏的要求。但是它也能确保更多的受众接触到你的文章，更重要的是，<em>相关</em>的受众。</p>
<p>你还可以决定选择 <a href="https://help.medium.com/hc/en-us/articles/360018834314-Stories-that-are-part-of-the-metered-paywall">metered paywall</a>（你可以从中赚钱！）：</p>
<blockquote>
<p>属于付费专区的故事也可以通过主题分发给 Medium 读者，这些主题可在我们的主页、主题页、每日摘要和应用程序中被推荐。</p>
</blockquote>
<p>我无法告诉你哪种方式更好，但是我个人更喜欢专栏，因为它能确保你的文章被读者看到，而不是像“符合发布条件”一词这么模糊。</p>
<p>如果你的博客广为传播，那么它就可以产生级联效应，为你的开源项目带来更多的人。</p>
<p>例如，如果你的 GitHub 项目在发表文章之后的一天之内收获了几十颗星，就可以进入 GitHub 的<a href="https://github.com/trending">趋势</a>页，这本身也是另一个暴露源。</p>
<p>让你的博文更加优秀的几个要点：</p>
<ul>
<li>以问题陈述作为开始，它甚至可以是博客的标题。<br>
<em>人们通常是在寻找某个特定问题的解决方案，在他们决定花时间读你的文章之前，他们应该知道你的文章是否是他们正在寻找的东西。这里是我写的一篇文章的<a href="https://medium.com/angular-in-depth/customizing-angular-cli-build-an-alternative-to-ng-eject-v2-c655768b48cc">示例</a>。</em><br>
<em>如你所见，它在标题中清晰地阐述了它所解决的问题。</em><br>
<em>如果你在谷歌搜索“Customizing Angular build”，它将会出现在排名靠前的几个结果中，并且你可以直接从搜索页面上看到它解决了哪个问题。</em></li>
<li>描述一下你的项目为什么要解决了这个问题，它是如何解决的。</li>
<li>提供一份详细的逐步指导，从安装开始，以可以正常运行的示例结束。<br>
<em>有很多开发者都更喜欢可以正常运行的示例，而不是博客文章。</em></li>
<li>在发表文章之前，先获取一些反馈。<br>
<em>让你的朋友们仔细阅读你的文章，不要告诉他们你的文章是关于什么的，看它们能够自己说出来。如果他们做不到，那么很有可能是你的文章不够清晰，你需要写得更加详细。</em></li>
</ul>
<p>在发表博文之后，在社交媒体上与你的朋友、家人和马路上的陌生人分享。</p>
<p>这将会增加你的项目的曝光度——但是你也要让人们<em>愿意</em>向你的项目做贡献。</p>
<h3 id="">项目如何吸引贡献者</h3>
<p>最好的办法就是与他人一起发起一个开源项目。通过这种方式，你从一开始就能拥有一个可以一起分担责任的团队。</p>
<p>然而，并不总是如此。</p>
<p>如果你独自发起开源项目，你必须吸引贡献者。根据我的经验，有两种类型的贡献者：</p>
<ol>
<li>想要产生影响并在找项目做贡献的人（虽然很少见，但是也有）。</li>
<li>使用你的程序包并且发现了缺陷或者缺少某些功能的人。</li>
</ol>
<p>在这两种情况下，只在 GitHub 上分享你的源代码并写一篇关于如何使用它的博文是不够的。以下是一些可以使人们愿意做贡献的事情：</p>
<h4 id="">一个待实现清单</h4>
<p>它可能包含已知缺陷、规划的功能或者其它的东西。这个清单会让第一类贡献者更容易选择正确的事项并发起 PR。</p>
<p>它可以是一个独立的清单，你也可以（或许是应该）使用 GitHub 上的议题（issues）和标签（labels）。</p>
<h4 id="">一份 <a href="https://docs.github.com/cn/github/building-a-strong-community/setting-guidelines-for-repository-contributors">贡献者指南</a></h4>
<p>基本的贡献者指南应该解释仓库的结构，包含关于构建并运行项目与测试的逐步指导。扩展的指南可以包含架构、设计决策、行为准则等。</p>
<p><a href="https://github.com/atom/atom/blob/master/CONTRIBUTING.md">Atom 的贡献者指南</a>就是一个很好的例子。千万不到低估它的价值！随着项目的发展，这需要花费大量的时间，我希望我从一开始就创建它，并随着项目的发展逐渐更新。</p>
<p>不幸的是，我没有人指出它的重要性，而<a href="https://github.com/just-jeb/angular-builders">我的项目</a>今天都还没有贡献者指南。它一直在我的待办清单上，但总有比它更紧急的事情。</p>
<h4 id="">感谢你的贡献者们</h4>
<p>在项目的主页列出贡献者们，这会让他们有更多的动力去做贡献。</p>
<p>只添加他们的名字就够了，但是我将会推荐你使用 <a href="https://github.com/all-contributors/all-contributors">All Contributors</a>。它不仅能为你的所有贡献者创建带有个人资料图片和徽标的精美部分，还能通过创建 PR 来自动添加新的贡献者，将贡献者添加到这个区域。</p>
<h3 id="">小结</h3>
<p>我们在这一部分讨论了增加项目曝光度以及赋予人们初始动力去创建 PR 或议题的几件事情。</p>
<blockquote>
<p>但是这并不能让他们坚持做贡献者，也不能确保他们完成已开始的工作。</p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_tyzBkDXaXjRW4UIEWBikzQ.jpeg" alt="1_tyzBkDXaXjRW4UIEWBikzQ" width="600" height="400" loading="lazy"></p>
<h2 id="">如何管理议题与拉取请求</h2>
<p>既然我们已经探索了共享信息和如何让你的开源项目变得更具吸引力，让我们来讨论一下<em>贡献</em>吧，它是每个开源项目的圣杯。</p>
<h3 id="">什么是开源贡献？</h3>
<p>对一个开源项目的贡献是指由所有者以外的人所做的任何改变。在实践中，它有两种形式：</p>
<h4 id="">议题</h4>
<p>这里是 GitHub 关于议题（issues） 的<a href="https://docs.github.com/cn/github/managing-your-work-on-github/about-issues">描述</a>：</p>
<blockquote>
<p><em>你可以在仓库中使用议题收集用户反馈，报告软件漏洞，并且组织要完成的任务。议题不只是一个报告软件漏洞的地方。</em></p>
</blockquote>
<p>简而言之，议题可以是需要采取某种行动的任何信息。</p>
<h4 id="pr">拉取请求（PR）</h4>
<p>这里是 GitHub 关于拉取请求（Pull Request，PR）的<a href="https://docs.github.com/cn/github/collaborating-with-issues-and-pull-requests/about-pull-requests">描述</a>：</p>
<blockquote>
<p><em>拉取请求可让你在 GitHub 上向他人告知你已经推送到仓库中分支的更改。在拉取请求打开后，你可以与协作者讨论并审查潜在更改，在更改合并到基本分支之前添加跟进提交。</em></p>
</blockquote>
<p>简而言之，拉取请求就是对项目的实际修改。</p>
<h3 id="pr">如何使用议题和 PR</h3>
<p>那么，你应该如何使用议题和 PR，又该如何处理贡献者创建的议题与 PR 呢？</p>
<h4 id="">以身作则</h4>
<p>我能给你的最好建议是，结合某个具体的工作方法<em>以身作则</em>。这意味着，当你开发新功能时，你应该为这个功能创建一个 PR，在它满足你所有的要求之后就进行合并。</p>
<p>你应该在发现缺陷或者一些缺失功能时创建议题。</p>
<p>这个方法不仅能组织好你的工作，让你的项目变得井井有条，还能给贡献者们提供一个参考，他们可以从中学习并调整自己的议题与 PR。</p>
<p>此外，如果你的标准很高（即你相信每个 PR 都应该有适当的文档、测试覆盖等等），那么你应该像对待任何其他贡献者一样对待你自己。你不能要求别人做你自己都没有做的事情。</p>
<p>还有就是，有时候你对贡献者应该比对自己更宽容。在你的项目处于初期阶段，没有很多贡献者时，更应该这样。这就涉及到了下面这一点。</p>
<h4 id="">感谢一切付出</h4>
<p>与他人协作就是要相互尊重。你应该尊重你的贡献者们，耐心地回答他们的问题（即便问题看起来很简单），礼貌地对待<em>建设性批评</em>。</p>
<p>记住：对贡献者工作的感谢至关重要。如果某人只是创建了一个议题（即使这个议题没有经过深入研究，甚至没有重现），感谢他们。他们费力地把自己的椅子挪到离桌子近一点的地方，坐直身子，然后打了一点他们认为对你有用的东西，感谢他们。如果需要的话，用礼貌而又尊重的方式向他们询问更多的细节。</p>
<p>如果某人创建的 PR 没有满足你的高标准，感谢他们。感谢他们并礼貌地请求他们修改代码/编写测试/添加文档，等等。给他们一个你的 PR 的链接作为参考，或者给他们一个贡献指南的链接。</p>
<p>建设性地积极对话将会给予那些贡献者们额外的动力，让他们继续工作。</p>
<h4 id="vs">质量 vs 数量</h4>
<p>最终，几乎总会有一个折衷（除非你自己拥有一个像 Angular 或 React 这样的大型开源项目）。你可以决定不放低标准，哪怕是一点点也不行，很有可能你最终会自己完成所有的工作。</p>
<p>或者，你可以决定放低对贡献者的标准（但是这可能会让你的标准显得毫无用处，因为它们没有被执行）。</p>
<p>我了解到，每个贡献者都需要使用不同的方法。这真的是由他们个人及其对贡献的兴趣决定的。</p>
<p>你应该考虑议题的紧急性、贡献者的经验、代码的复杂度、所需修复或功能的复杂度、贡献者的动机等因素。</p>
<p>通常，当议题非常重要时，我会礼貌地请求贡献者进行更改，然后等个几天，如果没有任何进展，我就会自己进行更改。至于那些没那么重要的（有了会更好）修复或者功能，我通常会把它们完全留给社区。</p>
<p>随着议题和 PR 数量的增长，跟踪、确定优先级并对它们进行分类就会成为一项艰巨的任务。这意味着标签会变得异常重要。</p>
<h4 id="">使用有用的标签</h4>
<p><a href="https://docs.github.com/cn/github/managing-your-work-on-github/managing-labels">GitHub 的标签</a>是让议题和 PR 保持优先级与组织性的好工具。虽然你可以通过标签进行搜索和过滤，但是我发现最有用的还是它可以帮助可视化项目的整体状态。</p>
<p>这样，你可以进入“议题”页，看到大部分议题都被打上了 <code>bug</code> 标签（这意味着你应该停下来集中精力修复它们，而不是往前推进了）。</p>
<p>或者，你可以看见大部分议题（issue）都被标记为 <code>enhancement</code> 或需要 <code>features</code>。<code>priority</code> 是另一个有用的标签，可以帮你首先关注到重要的东西。</p>
<p>此外，你的贡献者可以（也会）从你使用的标签中受益。例如，回到<strong>吸引贡献者</strong>，一些人可以进入议题页（issues），然后直观地地识别出那些需要社区帮忙处理的议题（<code>help-wanted</code>、<code>pr-welcome</code>，等等）。</p>
<p>除了职责单一的标签（比如 <code>bug</code> 或 <code>enchancement</code>）外，我推荐你使用标签来限定议题/PR 的范围。例如：</p>
<ul>
<li><code>priority:low</code>，<code>priority:high</code></li>
<li><code>required:investigation</code>，<code>required:tests</code>，<code>required:docs</code></li>
<li>或者在单仓库的情况下： <code>packages:package1</code>，<code>packages:package2</code> 等等</li>
</ul>
<p>这里是一个使用了标签的议题页，它来自我的项目：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-141634.png" alt="Screenshot-2021-03-12-141634" width="600" height="400" loading="lazy"></p>
<p>标签让你可以快速地分辨出哪些问题是需要你（或你的贡献者）注意的，这些问题与哪个组件相关以及需要什么才能继续进行。</p>
<h4 id="pr">使用 PR 和议题模板</h4>
<p>我强烈建议你花几分钟时间，为<a href="https://docs.github.com/cn/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository">议题</a>和 <a href="https://docs.github.com/cn/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository">PR</a> 定义模板。</p>
<blockquote>
<p><em>利用议题和拉取请求模板，可以自定义和标准化你希望贡献者在你的仓库中打开议题和拉取请求时加入的信息。</em></p>
</blockquote>
<p>这将为你节省大量的时间，因为你不需要对每个问题或 PR 进行附加信息或更改的请求。有时候你还是需要这样做（因为有些贡献者根本不关注模板），但是它发生的频率要比不创建模板少得多。</p>
<p>这里是默认议题的一个例子，你可以看到对应的模板是何时在你的仓库中定义的：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-141725.png" alt="Screenshot-2021-03-12-141725" width="600" height="400" loading="lazy"></p>
<h4 id="github">使用 GitHub 的应用程序与操作</h4>
<p>有很多的 GitHub <a href="https://docs.github.com/cn/actions/creating-actions/about-actions">应用程序和操作</a>可以帮你管理 PR 与议题。相关应用程序和操作的数量仍在不断增长，但是我个人发现这些功能最有用：</p>
<ul>
<li><a href="https://github.com/marketplace/stale">Stale bot</a></li>
<li><a href="https://github.com/marketplace/wip">WIP</a></li>
<li><a href="https://github.com/dkhmelenko/autoapproval">Autoapproval</a></li>
<li><a href="https://github.com/actions/labeler">PR labeler</a></li>
</ul>
<h4 id="">及时响应</h4>
<p>如果我在其它开源项目上打开了一个议题或 PR，并且等了很长时间才收到回复，那么我就会对它失去兴趣。<a href="https://github.com/greenkeeperio/monorepo-definitions/pull/24">这里</a>是一个例子：</p>
<ul>
<li>最初的响应非常快，只花了两天</li>
<li>讨论的成果丰富</li>
<li>PR 仍然处于打开状态，但却没有关于到底是什么少了/错了的更新</li>
</ul>
<p>最终，我转向了另一个包。</p>
<p>如果你不及时响应，这也会发生在你的项目上：如果你要花两周的时间响应一个需要你处理的 PR，而不是等待你要求的贡献者更改，那么你就会失去用户（即潜在的贡献者）。</p>
<p>所以帮你自己一个忙——及时响应。不一定要立马解决某人的问题，但是，即便是让用户知道你会在下周研究他们的议题，也给了他们一些确定性和时限。</p>
<p>坏消息是，你应该信守诺言。如果你的诺言有时没有完成，不要担心——我们所有人都有自己的生活，如果你因一些紧急的事情推迟了你在开源上的工作，是可以理解的。</p>
<p>如果发生了这种情况，就给一个简短的更新——又不是什么大事儿，只需要写一两个字，让人们知道他们一直在等待的那个功能被推迟了。</p>
<h3 id="">如何确定议题的优先级</h3>
<p>有几种方法可以帮你确定最重要议题的优先级。</p>
<p>首先，应该如何识别出最重要的议题呢？我个人认为，最重要的议题就是用户最想要的那些东西，不管它是新功能、缺陷修复，还是其它东西。</p>
<p>有时候，用户会在议题中表现出他们的兴趣，但是他们很有可能不会这么做。因此，我给大家介绍一种了解用户对哪些东西感兴趣的简单方法：</p>
<p>GitHub 上的每个项目都有一个 “Insights” 选项，其中有一部分叫 “Traffic"：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142214.png" alt="Screenshot-2021-03-12-142214" width="600" height="400" loading="lazy"></p>
<p>你可以在这部分的底部找到热门内容表（Popular Content table），它可以让你深入了解哪些页面是使用者们访问得最多的：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142309.png" alt="Screenshot-2021-03-12-142309" width="600" height="400" loading="lazy"></p>
<p>这个表中展示的议题是访问频率最高的那些议题，因此对你的用户来说，它们最有可能是最重要的。</p>
<p>甄别出最重要的议题后，你需要在议题页面突出它们。这里是几种方式：</p>
<h4 id="">固定议题</h4>
<p>每个仓库可以有最多三个固定议题（pinned issue）。固定议题出现在议题页面的顶部，所以几乎不可能忽略他们：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142429.png" alt="Screenshot-2021-03-12-142429" width="600" height="400" loading="lazy"></p>
<h4 id="">添加标签</h4>
<p>我们已经讨论过<em>使用</em>标签了，并且对 <code>help-wanted</code> 和 <code>priority:high</code> 标签的应用来说，这是一个绝佳的例子。这些标签会让潜在的贡献者们知道这个议题很重要，并且他们的任何帮助都会被感谢。</p>
<h4 id="">持续集成</h4>
<p>在将每个拉取请求合并到主干（master，或者 main）之前，先进行构建和测试，这将让你对即将合并到主干分支的代码充满信心（取决于测试的覆盖程度）。</p>
<p>尽管我不能不提到它是 PR 管理过程的一部分，但是它是一项任务的<em>自动化</em>，否则你不得不自己做，这样它就与 PR 管理没有直接关系了。</p>
<p>你仍然可以检出（check out）每个 PR，在本地构建，运行测试，然后在一切都通过之后进行合并（这样的话，持续集成就与 PR 管理没有直接关系）。不过别担心，我们将会在下一部分详细介绍持续集成。</p>
<h3 id="">小结</h3>
<p>让你的项目保持整洁有序非常重要，因为，我们都知道，整洁是一种美德。它不仅让管理过程更加高效，还能改善项目给人的总体印象。</p>
<blockquote>
<p>PR 和议题（以及代码库）是开源项目门面的不可或缺的一部分。不要低估它们的价值。</p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_n8_iSirZKBjHRufT6silGw.jpeg" alt="1_n8_iSirZKBjHRufT6silGw" width="600" height="400" loading="lazy"></p>
<h2 id="">如何实现流程自动化</h2>
<p>管理贡献（即议题和 PR）的一个自然部分是自动化——可能是 OSS 项目管理中最重要的方面之一。</p>
<h3 id="">为什么要自动化？</h3>
<p>如果说我在拥有一个开源系统的这些年里学到了什么，那就是你要做的例行事项越少，你就有越多的空闲时间用于实际的工作（比如修复缺陷或者开发新功能）。因此，我力求<strong>尽可能自动化</strong>。</p>
<p>这里是我希望我们如何实现这个目标的方式：首先检查两个工作流程（非自动化和全自动化），看你有多少时间实际上是花在例行事项上的。然后，我们将探讨如何实现改进的工作流程，让我们有更多的时间来修复缺陷。</p>
<h3 id="">最糟糕的情况——没有自动化</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142749.png" alt="Screenshot-2021-03-12-142749" width="600" height="400" loading="lazy"></p>
<p>如你所见，在没有任何自动化的情况下，所有的工作都由你来做。仅仅对于一个缺陷修复来说，你就需要做很多工作，更重要的是，<em>每次</em>修复缺陷或开发新功能时，你都要做这些工作！</p>
<p>现在我们来看看另一种情况。</p>
<h3 id="">最好的情况——一切都是自动化的</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142807.png" alt="Screenshot-2021-03-12-142807" width="600" height="400" loading="lazy"></p>
<p>在这种情况下，你只需要做必须要做的事情——检查代码和（偶尔）批准拉取请求，其他的一切都是自动完成的。</p>
<p>科幻小说？不，它被称为<strong>持续集成（continuous integration）</strong> 和 <strong>持续部署（continuous deployment）</strong>。在这里，我们并不会深入构建脚本和特定系统配置的细节。相反，我们将会查看让它发挥作用所需的工具，我将会让你自己决定具体细节。</p>
<h3 id="ci">什么是持续集成（CI）?</h3>
<blockquote>
<p><em>持续集成（CI）是一种自动将代码改动从多个贡献者集成到单个软件项目中的实践。 CI 过程由自动工具组成，这些工具会在集成之前断言新代码的正确性。</em></p>
</blockquote>
<p>一个非常基础的 CI 运行将会包括 <strong>构建（build）</strong> 和 <strong>单元测试（unit tests）</strong>，但是并不局限于这两种。可能也会包含各种各样的静态代码分析工具、链接器等等。这里的标准由你定。</p>
<h3 id="">为什么你应该使用端到端测试</h3>
<p>构建和单元测试可以为你提供有关代码变动的快速反馈，所需时间相对较短，并且在出现问题的时候迅速失败。但是端到端（end-to-end，E2E）测试在 CI 中有着特殊的地位。</p>
<p>端到端测试不仅应该覆盖代码的正确性，还应该覆盖到你的部署流程、包的完整性等等。</p>
<p>我自己也意识到了这一点，当我不小心发布了一个不包含代码的新版本包。构建通过了，单元测试和端到端测试也没有问题（这一次是通过链接测试项目的构建输出目录来安装的）。哪里失败了呢？在打包阶段。</p>
<p>这里有一个关键点：端到端测试应该像真实用户使用那样测试你的软件包。</p>
<p>为了达到这个目标，我推荐以下几步：</p>
<ol>
<li>在你的 CI 运行期间，启动一个本地的包注册中心。每个语言/生态系统都有几个选择，例如对 Java 或 Scala 项目来说，你可以用 <a href="https://blog.sonatype.com/using-nexus-3-as-your-repository-part-1-maven-artifacts">Nexus 仓库</a>，对 JavaScript 来说，可以使用 <a href="https://github.com/verdaccio/verdaccio">Verdaccio</a>（我在 <a href="https://github.com/just-jeb/angular-builders">@angular-builders</a>中使用它）。</li>
<li>有一个使用你的软件包的独立项目（它可以位于同一个仓库中）。这个仓库中的测试应该测试你的打包功能。</li>
<li>配置这个项目使用本地包注册中心。</li>
<li>构建完你的包之后，将其发布到本地包注册中心（在你的 CI 系统中启动）</li>
<li>安装改包的最新版本（你刚才构建的那个）到你的测试项目中。</li>
<li>运行测试。</li>
</ol>
<p>这不仅可以测试软件包的完整性和可靠性，还可以在进行持续部署时给你省去一些工作。</p>
<h3 id="ci">CI 系统是如何工作的</h3>
<p>很多 CI 系统都有针对开源项目的免费计划，其中有 <a href="https://travis-ci.com/">Travis CI</a>、<a href="https://circleci.com/">CircleCI</a>、<a href="https://www.appveyor.com/">AppVeyor</a>、<a href="https://github.com/features/actions">GitHub Actions</a> 等。</p>
<p>它们的功能都比较多，做的事情也基本相同：检出你的代码到虚拟机、运行你定义的脚本（通常运行构建和测试），然后向 GitHub 报告成功或失败。</p>
<p>所有这些系统都有一个用于和 GitHub 集成的 <a href="https://github.com/marketplace?category=continuous-integration&amp;type=apps">应用程序</a>，它们当中的集成过程也非常类似：</p>
<ol>
<li>在平台上注册。</li>
<li>在你的 GitHub 账户中安装对应的应用程序。</li>
<li><a href="https://github.com/settings/installations">配置对所选仓库的访问</a>。</li>
<li>创建一个配置文件（比如 <code>travis.yaml</code>），定义构建矩阵、所需构建链和 CI 脚本。</li>
<li>将它推送到主干。</li>
</ol>
<p>这会使你的 CI 在每个 PR 上运行，并向 GitHub 报告状态——但是这还不够。你真正想要的是在 PR 通过所有检查之前，阻止合并到主干分支。</p>
<p>这可以通过定义分支保护规则来实现。为了定义这些规则，你需要前往你仓库的 <strong>Setting</strong> 中的 <strong>Branch</strong> 部分，然后点击 <strong>Add rule</strong> 按钮：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142547.png" alt="Screenshot-2021-03-12-142547" width="600" height="400" loading="lazy"></p>
<p>然后选择多选框 <strong>Require status checks to pass before merging</strong>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot-2021-03-12-142635.png" alt="Screenshot-2021-03-12-142635" width="600" height="400" loading="lazy"></p>
<p>如你所见，相应的 GitHub Apps 多选框也已经出现在了这里，所以剩下的唯一一件事情就是启用它们。</p>
<p>具体的构建脚本由你的生态系统、编写项目的语言、你所使用的框架等决定。因此，我不会在这里进行介绍——你需要自己检查 CI 系统的文档，了解具体细节。然而，你现在对什么是 CI 以及它是如何自动化你的 PR 有了很好的认识，让我们继续前进吧。</p>
<h3 id="cd">持续部署（CD）是如何工作的</h3>
<blockquote>
<p><em>持续部署（CD）是一个软件发布过程，它使用自动化测试来验证对代码库的更改是否正确和稳定，以便立即自动部署到生产环境中。</em></p>
</blockquote>
<p>在我们的情况中，生产环境就是程序包在包注册中心中公开可用的时候。这是一个无法返回的阶段，因为一旦发布，就不能取消发布了，因为程序包是公开可用的（因此，它可能正被使用）。</p>
<p>持续部署有多种策略，具体策略取决于项目及其复杂程度。但是在我看来，发行（release）只应该基于主干分支，因为这会让工作流变得非常简单。具体做法如下：</p>
<ol>
<li>每个 PR 要么代表一个缺陷修复，要么代表一个新功能。</li>
<li>代码在进入主干之前经过了测试（包括端到端测试）。</li>
<li>主干分支是受保护的分支，所以只要你不合并失败的 PR，它就会保持稳定。</li>
<li>每个合并到主干的 PR 都会触发主干 CI 运行，CI 最终会发布一个新版本。</li>
</ol>
<p>这将确保所有的发布都是按顺序进行的，并且可以很容易地将某些 PR 与特定的版本联系起来。</p>
<p>为了自动化程序包的发行过程，你需要做几件事情：</p>
<ol>
<li>基于提交信息自动升级版本。</li>
<li>基于提交信息自动更新 CHANGELOG。</li>
<li>自动发布程序包到公共程序包仓库。</li>
<li>自动在 GitHub 上发行。</li>
</ol>
<p>给大家带来一个好消息：语义化版本（<a href="https://github.com/semantic-release/semantic-release">semantic-release</a>）已经支持所有这些功能了。坏消息是：你需要花一些时间才能让它发挥作用（但是最终会有所回报）。</p>
<h3 id="semanticrelease">Semantic-release 是如何工作的</h3>
<blockquote>
<p><em>semantic-release 自动化整个程序包发行工作流程，包括：确定下个版本号、生成发行说明和发布程序包。</em></p>
<p><em>这消除了人类感情和版本号之间的直接联系，严格遵循 <a href="http://semver.org/">语义化版本</a> 版本规范。</em></p>
</blockquote>
<p>我们不会在这里介绍整个集成过程，因为它们有良好的文档，也没有理由在这里进行复述。不过，我还是会提几点：</p>
<ul>
<li>在你开始 Semantic Release 之前，确保你理解 <a href="https://semver.org/">语义化版本声明</a> 和 <a href="https://www.conventionalcommits.org/en/v1.0.0/">约定式提交</a> 的格式。</li>
<li>为了使 semantic-release 能够良好地工作，你应该强制执行某些提交消息格式。为此，你可以将 <a href="https://github.com/conventional-changelog/commitlint">Commitlint</a> 作为一个 <a href="https://github.com/typicode/husky">husky</a> 预提交钩子运行。当有人创建本地提交时，它将强制执行常规提交，但对于直接从 GitHub Web UI 进行的提交就无能为力了（这通常发生在有人想要快速修复他们的 PR 时）。因此，我建立你通过 <a href="https://github.com/marketplace/actions/commit-linter">commitlint GitHub Action</a> 对其进行备份。</li>
</ul>
<p>在将语义化发行设置为工作流的一部分之后，你差不多就快完成了，你不再需要在这些常规过程上花时间。尽管你还可以进行另一项优化。</p>
<h3 id="">如何保持项目的更新</h3>
<p>如果你的项目没有外部依赖，跳过这一部分。然而，大多数项目都依赖于其它程序包，而其它程序包往往会发生变化。</p>
<p>使项目保持最新的依赖关系很重要，但这很耗时。幸运的是，我们有一个解决方案。实际上，有一些，例如 <a href="https://greenkeeper.io/">Greenkeeper</a>、<a href="https://renovate.whitesourcesoftware.com/">Renovate</a> 和 <a href="https://dependabot.com/">Dependabot</a>。</p>
<p>它们的想法几乎相同，因此我只引用 Dependabot 的 “How it works” 部分：</p>
<blockquote>
<p><strong>1. Dependabot checks for updates</strong><br>
<em>Dependabot pulls down your dependency files and looks for any outdated or insecure requirements.</em></p>
</blockquote>
<blockquote>
<p><strong>2. Dependabot opens pull requests</strong><br>
<em>If any of your dependencies are out-of-date, Dependabot opens individual pull requests to update each one.</em></p>
</blockquote>
<blockquote>
<p><strong>3. You review and merge</strong><br>
<em>You check that your tests pass, scan the included changelog and release notes, then hit merge with confidence.</em></p>
</blockquote>
<p>你可能已经注意到，他只在你有能发挥作用的 CI 时才有意义。</p>
<h3 id="">小结</h3>
<p>如果你有一个全自动化的 CI/CD 闭环，并且在你的 OSS 仓库中有一个新打开的议题，你可以在几分钟内提供一个缺陷修复。</p>
<p>实际上，你可以在你的手机上进入 GitHub 移动版，修复一两行缺陷代码，然后提交。剩余的事情就自动完成了，你的客户马上就能得到一个新的版本。</p>
<p>我自己就能够快速、轻松地向客户多次提供修复版本。</p>
<blockquote>
<p><em>拥有强大的自动化能力并不是为了腾出一些时间进行休闲娱乐，而是要把时间用在真正重要的事情上，并提高响应能力。</em></p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_6k7J2Dj1iz0c901UExzjWg.jpeg" alt="1_6k7J2Dj1iz0c901UExzjWg" width="600" height="400" loading="lazy"></p>
<h2 id="">版本管理</h2>
<p>在这篇指南的最后，我想谈一谈版本管理，对于任何拥有大量用户的 OSS 项目来说，版本管理总是很重要。你将会了解到版本符号、中断性变更、向后移植，等等。</p>
<h3 id="">什么是软件版本控制？</h3>
<p>咱们来看一下维基百科对软件版本控制（software versioning）的解释吧。</p>
<blockquote>
<p><em>软件升级版本控制（Software upgrade versioning）是将唯一的版本名称或版本号分配给<a href="https://en.wikipedia.org/wiki/Computer_software">计算机软件</a>的唯一状态的一个过程。</em></p>
<p><em>现代计算机软件通常采用两种不同的软件版本控制方案进行版本跟踪——可能在一天内增长很多次的<a href="https://en.wikipedia.org/wiki/Software_versioning#Internal_version_numbers">内部版本号</a>，比如修订控制号，还有一个就是发行版本，它变化得通常没这么快，比如语义化版本<a href="https://en.wikipedia.org/wiki/Software_versioning#cite_note-semver-1">[1]</a>或<a href="https://en.wikipedia.org/wiki/Code_name#Project_code_name">项目代号</a>。</em></p>
</blockquote>
<p>确实，有很多方法可以唯一标识你的软件产品的版本。</p>
<p>最广为人知的方式就是给它起一个名字。</p>
<p>地球上的绝大多数人，甚至包含那些与技术有着间接关系的人，都可能听说过安卓的冰淇淋三明治（Ice Cream Sandwich）和棉花糖（Marshmallow）或 Mac OS 的美洲豹（Leopard），以及它的冷冻表亲雪豹（Snow Leopard），还有 Big Sur。</p>
<p>程序员可能听过 Eclipse 及其天体版本 Luna、Mars 和 Photon。</p>
<p>所有这些都是软件产品的大版本号。</p>
<p>尽管名字非常适合市场营销，但是它们有时候也会让人感到困惑。</p>
<p>实际上，谷歌已经在它们的安卓版本名字中取消了对糖果的使用，因为它们：</p>
<blockquote>
<p><em>多年以来都听到用户在反馈：这些名字总是不能让全球社区中的每个人都有直观的理解。</em></p>
</blockquote>
<p>没错，但也许只是我们还没进化到从动物种类推断出版本号的程度，尽管雪豹（Snow Leapard）比美洲豹（Leopard）要酷很多。</p>
<p>天体和糖果都是比较容易理解的概念，但前提是你必须按照字母的出现顺序命名（像安卓和 Eclipse 这样）。但是有一点可以肯定——没有什么方法能够比数字更好地确定连续的情况。</p>
<p>因此，如果你将你的软件产品的第一个版本命名为“Product 1”，将第二个版本命名为“Product 2”，那么就可以很直观地说第二个版本是最新的，不是吗？</p>
<p>然而，不同于不暴露 API 的独立软件产品，那些被其他软件（比如 OSS 产品主体部分）使用的软件需要更好的版本控制，而不仅仅是一串数字。</p>
<p>例如，如果我们用一个简单的数字序列进行版本控制，用户如何能区分出缺陷修复和中断现有 API 的变更呢？</p>
<p>答案就是……语义化版本。</p>
<h3 id="">什么是语义化版本？</h3>
<p>语义化版本（又称 SemVer）是一个广泛采用的版本管理方案，它使用格式为 <code>MAJOR.MINOR.PATCH</code> 的三位数字序列。</p>
<p>规则很简单——给定一个版本号 <code>MAJOR.MINOR.PATCH</code>，分别在不同的情况下递增不同的版本：</p>
<ul>
<li>当你进行了不兼容的 API 变更时，递增 <code>MAJOR</code> 版本</li>
<li>当你以向后兼容的方式添加了一个功能时，递增 <code>MINOR</code> 版本</li>
<li>当你进行了向后兼容的缺陷修复时，递增 <code>PATCH</code> 版本</li>
</ul>
<p>预发行和构建元数据的其他标签可以作为 <code>MAJOR.MINOR.PATCH</code> 格式的扩展使用。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/versioning.png" alt="versioning" width="600" height="400" loading="lazy"></p>
<p>它提供了一种简洁明了的方式，来将软件产品中的变更传递给你的用户。</p>
<p>但是最重要的是，它被所有种类的包管理器和构建工具（比如 <a href="https://docs.npmjs.com/about-semantic-versioning#using-semantic-versioning-to-specify-update-types-your-package-can-accept">NPM</a> 和 <a href="https://docs.oracle.com/middleware/1212/core/MAVEN/maven_version.htm#MAVEN8903">Maven</a>）广泛使用，这些工具允许用户依赖 <em>某个范围内</em> 的版本，而不是某个特定的版本。</p>
<p>例如，声明版本区间 <code>^2.2.1</code> 而不是显式的版本号 <code>2.2.1</code> 将会让用户接受任何向后兼容的缺陷修复或将会在 <code>2.2.1</code> 版本之上发布的新功能。</p>
<p>也就是说，构建工具和包管理器依赖用户和包的所有者之间的约定——这个约定是由 SemVer 定义的。</p>
<p>那意味着你要全权负责——你就是那个给中断性变更和小型变更下定义的人。你可以不小心将一个中断性变更作为缺陷修复（补丁版本）发布，并且它<em>将会</em>破坏依赖某个范围的构建。</p>
<p>破坏构建是一件非常恐怖的事情，所以我推荐你使用带有预定义消息格式的 <code>semantic-release</code> 和提交格式的强制工具。</p>
<p>你可以在 <a href="https://semver.org/">Server.org</a> 的官网找到更多有关语义化版本的信息。</p>
<p>既然我们已经学了如何<em>识别</em>中断性变更，我们就来聊聊如何<em>引入</em>它们吧。</p>
<h3 id="">如何管理中断性变更</h3>
<p>中断性变更（breaking change）是那些对公开 API 的变更，这些变更以不兼容的方式移除、重命名或更改了你与用户之间的约定。</p>
<p>理想情况下，你将会在你的代码中保持向后兼容，并且永远不会引入中断性变更。但是，你会意识到现实的残酷的。</p>
<p>软件在不断演进，你的代码也是。用户的需求会变，你的 API 也会变。你作为一名开发者在成长，你的产品也在成长。</p>
<p>因此，特别是作为一个不拿工资的开源开发者，你就是不能容许你自己维护的项目中存在的所有遗留代码。有时，你需要移除它们。</p>
<p>问题是如何移除？</p>
<p>与往常一样，需要进行权衡。你会更清楚这个或其它的变更对用户的影响。</p>
<p>你不必不惜代价地保持向后兼容，也不必在每个旧版本中实现所有的新功能。但是，这毫无疑问是你<em>应该</em>考虑到的事情。</p>
<p>如果用户的迁移成本比较低，那么进行中断性变更是可以的，在较老的版本中不支持这个功能也很合理。</p>
<p>然而，如果迁移成本很高，绝大多数用户无法承担的话，你或许应该先考虑让这个变更向后兼容，然后发布一个废弃警告。</p>
<p>废弃警告通常和新的 API 一起发布，旧的 API 仍然受到支持。这样一来，用户就有时间进行迁移。在他们完成迁移之后，你就可以再下个大版本中安全地移除废弃警告和旧的 API 了。</p>
<p>无论如何，不管你何时引入中断性变更，都要确保有一份迁移指南，包含迁移的每一步。</p>
<p>此外，出于礼貌，你最好给用户留下为中断性变更做准备的时间，尤其是在没有宽限期的情况下（新旧 API 都支持）。</p>
<p>一个解释中断性变更、其背后的原因以及预期的时间范围的预先通知是非常有用的。这个通知可以是一条推特、一篇博客文章，甚至是任何带有废弃警告的新的小版本。</p>
<p>记住，虽然中断性变更基本上是一个负面体验，但是一个<em>突然</em>的中断性变更却是一个<em>非常</em>负面的体验。</p>
<h3 id="">自动迁移</h3>
<p>我们可以将中断性变更分为两类——非确定性（non-determinstic）变更与确定性（determinstic）变更。</p>
<p>非确定性变更是指那些你无法预测迁移工作结果的变更，比如将某个 API 的特定部分完全移除。</p>
<p>在这种情况下，由用户自己决定是否要用第三方库替换它、自己实现它。</p>
<p>确定性变更是指那些给定代码 <code>X</code> 和用户输入 <code>I</code>，允许你将其转变为代码 <code>Y</code> 的变更（即通过输入可以明确地知道输出）。比如，改变函数名或者导入语句。</p>
<p>如果你引入了一个确定的中断性变更，你可以编写一个自动化程序，修改用户的代码库，并将其调整为新的 API。</p>
<p>有了这种自动化，你就不必担心向后兼容和详细的迁移指南了。你给用户提供一种不需任何努力就能升级他们代码的方式，这对软件更新至关重要。</p>
<p>然而，这里也存在固有的权衡。写代码需要花时间，就像编写迁移指南一样。当然，编写将复杂代码流迁移到新 API 的代码比编写替换函数名的代码花费更多的时间。</p>
<p>有时，你并不能负担起这个时间。</p>
<p>如果你决定这么做，有一些工具可以帮你实现目标。</p>
<p>其中最广为人知且语言无关的就是 Facebook 的 <a href="https://github.com/facebook/codemod">Codemode</a>。</p>
<blockquote>
<p><em>codemod 是一个工具/库，可以帮助你重构大规模代码库，重构可以部分自动化，但仍然需要人工监督和偶尔干预。</em></p>
</blockquote>
<p>还有一些更加复杂的工具，它们使用 <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">抽象语法树（AST）</a>，可以用于更加复杂的任务，而不仅仅是查找并替换。</p>
<p>例如，Facebook 的另一个库（只适用于 JS/TS）被称为 <a href="https://github.com/facebook/jscodeshift">JSCodeShift</a>。或者 <a href="https://github.com/ranyitz/code-migrate">code-migrate</a>——一个允许你比较容易地编写迁移指南的工具（还是只适用于 JS/TS），为用户提供一个漂亮的基于命令提示符的 CLI 界面。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/1_aFlF8Vx0-thA0EutbBgiUA.png" alt="1_aFlF8Vx0-thA0EutbBgiUA" width="600" height="400" loading="lazy"></p>
<p>一些大型 OSS 项目甚至有他们自己的解决方案。其中有一个例子就是 <a href="https://angular.io/guide/schematics">Angular schematics</a> ——一个基于模板的代码生成器，支持复杂逻辑。</p>
<p>自动代码迁移可以作为一个单独的程序包（比如 <code>my-cool-oss-migrate-v4-v5</code>）发布，并作为迁移指南中的一个步骤被提到。</p>
<p>另外，迁移可以是包含中断性变更的大版本的一部分，可以在用户在代码库中安装该版本后执行。具体由你决定。</p>
<h3 id="">移植</h3>
<p>另一个惯例就是将重要的变更移植到以前的版本中。例如，在某个主要发行版本（此版本包含中断性变更）之后发现了一个严重缺陷，但是这个缺陷也存在于之前的版本中。</p>
<p>这时，你不能指望用户们因为这一个缺陷就去进行繁琐的迁移。相反，检出老的修订版本，在它的上面进行修复，然后以该老版本的小版本发行，可能会很复杂。</p>
<blockquote>
<p><em>解决方案：为每个大版本建立一个受保护的分支。</em></p>
</blockquote>
<p>每当你计划发行一个大版本时，就从主分支创建出一个命名为 <code>c.x.x</code> 的分支，其中 <code>c</code> 就是当前的大版本号。把所有这样的分支设置为受保护的分支（就像主分支一样），这样你就不会在不经意间破坏到它们。然后，在你不得不从一个新的大版本中移植某个功能或缺陷修复的任何时候，你可以在这个分支上重新实现它，也可以（如果可能的话）从主分支挑选（cherry-pick）对应的提交。</p>
<p>此外，有一个策略值得一提：为<em>下一个</em>大版本创建一个单独的分支（而不是只为之前大版本创建分支）。</p>
<p>这通常适用于大规模项目（比如 Webpack 或 Babel），这些项目在每个新的大版本中多有很多的变更。</p>
<p>为即将到来的大版本建立一个单独的分支允许在其上开展工作，并将其发布进行测试，同时仍然将最相关的版本（在其上开展工作）保留在主分支。</p>
<p>新版本发布之后，它的分支就成为主分支，下个大版本的新分支也会被创建。</p>
<h2 id="">最后的想法</h2>
<p>我希望你喜欢这篇指南，现在已经对拥有一个开源项目意味着什么有了一个更好的理解。</p>
<p>最后，我想和大家分享一件事，在拥有一个开源项目时，你应该使用牢记。</p>
<h3 id="">倾听用户的声音</h3>
<p>这听起来可能有点违反直觉，但事实就是这样——你并不是唯一一个定义路线图的人，用户也定义了它。实际上，用户定义了路线图的绝大部分。</p>
<p>如果你拥有一个开源项目，你维护它的目的是为了帮助他人，而不是你自己。</p>
<p>准备多个反馈渠道。有些用户只有一个快速的问题，你可以在一秒钟内给出答案。</p>
<p>也有潜在的贡献者想要讨论路线图，但是他们又不想公开讨论，那么给他们一个联系你的途径。提供一个 Slack 或 Discord 的链接，分享你的 Twitter 账户，等等。渠道越多越好。</p>
<p>说到渠道，如果你有任何问题或想法，可以随时在 <a href="https://twitter.com/_Just_JeB_">Twitter</a> 上直接给我发消息。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/ultimate-owners-guide-to-open-source/">How to Be a Good Open Source Project Owner – The Ultimate Guide</a>，作者：<a href="https://www.freecodecamp.org/news/author/jeb/">JeB Barabanov</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ C 语言入门手册：几小时内就能学会的 C 语言基础 ]]>
                </title>
                <description>
                    <![CDATA[ 本手册遵循二八定律。你将在 20% 的时间内学习 80% 的 C 编程语言。 这种方式将会让你对这门语言有一个全面的认识。 本手册并不会尝试覆盖与 C 有关的一切。它只会关注这门语言的核心部分，尽量将更加复杂的主题简单化。 提示：你可以从这里获得这本手册的 PDF 或 ePub 版本 [https://flaviocopes.com/page/c-handbook/]。 尽情享受吧！ 目录  1.  C 语言简介  2.  变量与类型  3.  常量  4.  运算符  5.  条件语句  6.  循环  7.  数组  8. ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-c-beginners-handbook/</link>
                <guid isPermaLink="false">60331fa6c354c605689ea5b1</guid>
                
                    <category>
                        <![CDATA[ C语言 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Mon, 22 Feb 2021 03:09:20 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/coverc-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本手册遵循二八定律。你将在 20% 的时间内学习 80% 的 C 编程语言。</p>
<p>这种方式将会让你对这门语言有一个全面的认识。</p>
<p>本手册并不会尝试覆盖与 C 有关的一切。它只会关注这门语言的核心部分，尽量将更加复杂的主题简单化。</p>
<p>提示：<a href="https://flaviocopes.com/page/c-handbook/">你可以从这里获得这本手册的 PDF 或 ePub 版本</a>。</p>
<p>尽情享受吧！</p>
<h2 id="">目录</h2>
<ol>
<li><a href="#introduction-to-c">C 语言简介</a></li>
<li><a href="#variables-and-types">变量与类型</a></li>
<li><a href="#constants">常量</a></li>
<li><a href="#operators">运算符</a></li>
<li><a href="#conditionals">条件语句</a></li>
<li><a href="#loops">循环</a></li>
<li><a href="#arrays">数组</a></li>
<li><a href="#strings">字符串</a></li>
<li><a href="#pointers">指针</a></li>
<li><a href="#functions">函数</a></li>
<li><a href="#input-and-output">输入与输出</a></li>
<li><a href="#variable-scope">变量作用域</a></li>
<li><a href="#static-variables">静态变量</a></li>
<li><a href="#global-variables">全局变量</a></li>
<li><a href="#type-definitions">类型定义</a></li>
<li><a href="#enumerated-types">枚举类型</a></li>
<li><a href="#structures">结构体</a></li>
<li><a href="#command-line-parameters">命令行参数</a></li>
<li><a href="#header-files">头文件</a></li>
<li><a href="#the-preprocessor">预处理器</a></li>
<li><a href="#conclusion">结语</a></li>
</ol>
<h2 id="introduction-to-c">C 语言简介</h2>
<p>C 可能是最广为人知的编程语言。它被全世界的计算机科学课程中用作参考语言，除了 Python 与 Java，它可能是人们在学校学得最多得编程语言。</p>
<p>我记得它是我在 Pascal 之后的第二门编程语言。</p>
<p>学生们用 C 来学习编程，但它的作用远不止这一点。它不是一门学术型语言。它不是最简单的语言，因为 C 是一门非常底层的编程语言。</p>
<p>今天，C 在嵌入式设备中广泛使用，它驱动着绝大多数用 Linux 搭建的因特网服务器。Linux 内核是用 C 写的，这也意味着 C 驱动着所有安卓设备的内核。可以这么说，此时此刻，整个世界的一大部分就是由 C 代码运行的，令人惊叹。</p>
<p>在诞生之初，C 被认为是一门高级语言，因为它可以在不同机器之间移植。如今，我们或多或少都认为在 Mac 或 Windows 或 Linux 运行一个程序（可能使用 Node.js 或 Python）是理所当然的。</p>
<p>在以前，完全不是这样的。C 带来了一门易于实现的语言，它的编译器可以很容易地被移植到不同的机器上。</p>
<p>我说下译器：C 是一门编译型语言，就像 Go、Java、Swift 或 Rust 一样。其它流行的语言，比如 Python、Ruby 或 JavaScript 都是解释型语言。编译型语言与解释型语言的差别是不变的：编译型语言生成的是可直接执行和分发的二进制文件。</p>
<p>C 不支持垃圾收集（garbage collection），这意味着我们必须自己管理内存。管理内存是一项复杂的任务，需要十分小心才能预防缺陷，但 C 也因此成为了嵌入式设备（例如 Arduino）编程的理想语言。</p>
<p>C 并不会隐藏下层机器的复杂性和能力。一旦知道你能做什么，你就能拥有巨大的能力。</p>
<p>现在，我想介绍第一个 C 程序，我们将会管它叫“Hello, World”。</p>
<p>hello.c</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
    printf("Hello, World!");
}
</code></pre>
<p>让我们描述一下这段程序源代码：我们首先导入了 <code>stdio</code> 库（<code>stdio</code> 表示的是标准输入输出库（standard input-output library））。</p>
<p>这个库允许我们访问输入/输出函数。</p>
<p>C 是一门内核非常小的语言，任何内核以外的部分都以库的形式提供。其中一些库由普通编程人员构建并供他人使用。另一些库被内置在编译器中，比如 <code>stdio</code> 等。</p>
<p><code>stdio</code> 库提供了 <code>prinf()</code> 函数。</p>
<p>这个函数被包裹在 <code>main()</code> 函数中，<code>main()</code> 函数是所有 C 程序的入口。</p>
<p>但是，究竟什么是函数呢？</p>
<p>函数（function）是一个例程，它接收一个或多个参数并返回一个值。</p>
<p>在 <code>main()</code> 的例子中，函数没有参数，返回一个整数。我们使用 <code>void</code> 关键字标识该参数，使用 <code>int</code> 关键字标识返回值。</p>
<p>函数有一个由花括号包裹的函数体，函数需要进行的所有操作的代码都在函数体内。</p>
<p>如你所见，<code>printf()</code> 函数的写法稍有不同。它没有定义返回值，并且我们给它传入了一个用双引号包裹的字符串。我们并没有声明参数的类型。</p>
<p>那是因为这是一个函数调用。在 <code>stdio</code> 库中的某个地方，<code>printf</code> 被定义成</p>
<pre><code class="language-c">int printf(const char *format, ...);

</code></pre>
<p>你现在不需要理解这是何含义，简单来说，这是就是函数定义。当我们调用 <code>printf("Hello, World!");</code> 时，这就是该函数运行的地方。</p>
<p>我们在上面定义的 <code>main()</code> 函数：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
    printf("Hello, World!");
}
</code></pre>
<p>将会在程序被执行的时候由操作系统运行。</p>
<p>我们如何执行一个 C 程序呢？</p>
<p>如我所说，C 是一门编译型语言。要运行程序，我们必须先编译它。任何 Linux 或 macOS 计算机都自带了 C 编译器。至于 Windows，你可以使用适用于 Linux 的 Windows 子系统（WSL）。</p>
<p>无论如何，你都可以在打开终端时输入 <code>gcc</code>，这个命令应该会返回一个错误，提示你没有声明任何文件：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.10.50.png" alt="Screen-Shot-2020-01-29-at-10.10.50" width="600" height="400" loading="lazy"></p>
<p>很好。它说明 C 编译器是有的，现在我们可以开始使用它了。</p>
<p>现在将上面的程序输入到一个名为 <code>hello.c</code> 的文件中。你可以使用任何编辑器，不过为了简单起见，我将在命令行中使用 <code>nano</code> 编辑器：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.11.39.png" alt="Screen-Shot-2020-01-29-at-10.11.39" width="600" height="400" loading="lazy"></p>
<p>输入程序：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.16.52.png" alt="Screen-Shot-2020-01-29-at-10.16.52" width="600" height="400" loading="lazy"></p>
<p>现在按 <code>ctrl-X</code> 退出：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.18.11.png" alt="Screen-Shot-2020-01-29-at-10.18.11" width="600" height="400" loading="lazy"></p>
<p>按 <code>y</code> 键确认，然后按回车键确认文件名：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.18.15.png" alt="Screen-Shot-2020-01-29-at-10.18.15" width="600" height="400" loading="lazy"></p>
<p>就是这样，我们现在应该已经回到终端了：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.13.46.png" alt="Screen-Shot-2020-01-29-at-10.13.46" width="600" height="400" loading="lazy"></p>
<p>现在输入</p>
<pre><code class="language-sh">gcc hello.c -o hello

</code></pre>
<p>程序应该不会给你任何错误信息：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.16.31.png" alt="Screen-Shot-2020-01-29-at-10.16.31" width="600" height="400" loading="lazy"></p>
<p>但是它应该已经生成了一个名为 <code>hello</code> 的可执行程序。现在输入</p>
<pre><code class="language-sh">./hello

</code></pre>
<p>运行它：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.19.20.png" alt="Screen-Shot-2020-01-29-at-10.19.20" width="600" height="400" loading="lazy"></p>
<p>我在程序名的前面加了 <code>./</code>，告诉终端要执行的命令就在当前目录下。</p>
<p>太棒了！</p>
<p>现在，如果你调用 <code>ls -al hello</code>，你能看到这个程序只有 12KB 大：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-01-29-at-10.19.55.png" alt="Screen-Shot-2020-01-29-at-10.19.55" width="600" height="400" loading="lazy"></p>
<p>这是 C 的优点之一：它是高度优化的，这也是它非常适用于资源非常有限的嵌入式设备的原因之一。</p>
<h2 id="variables-and-types">变量与类型</h2>
<p>C 是一门静态类型语言。</p>
<p>这意味着任何变量都有一个相关联的类型，并且该类型在编译时是可知的。</p>
<p>这与你在 Python、JavaScript、PHP 和其它解释型语言中使用变量的方式大有不同。</p>
<p>当你在 C 中创建变量时，你必须在声明中给出该变量的类型。</p>
<p>在这个示例中，我们初始化一个 <code>int</code> 类型的变量 <code>age</code>：</p>
<pre><code class="language-c">int age;

</code></pre>
<p>变量名可以包含任意大写或小写字母，也可以包含数字和下划线，但是不能以数字开头。<code>AGE</code> 和 <code>Age10</code> 都是有效的变量名，但 <code>1age</code> 就不是了。</p>
<p>你还可以在声明中初始化变量，给出初始值即可：</p>
<pre><code class="language-c">int age = 37;

</code></pre>
<p>变量一旦声明，你就可以在程序代码中使用它了。你在任何时候都可以使用 <code>=</code> 改变它的值，例如 <code>age = 100;</code>（提供的新值的类型与原值相同）。</p>
<p>在这种情况下：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
    int age = 0;
    age = 37.2;
    printf("%u", age);
}
</code></pre>
<p>编译器会在编译时发出警告，然后将小数转为整数。</p>
<p><a href="http://localhost:4000/c-introduction">C</a> 的内置数据类型有 <code>int</code>、<code>char</code>、<code>short</code>、<code>long</code>、<code>float</code>、<code>double</code>、<code>long double</code>。咱们进一步了解这些数据类型吧。</p>
<h3 id="">整数</h3>
<p>C 给我们提供了下列定义整数的类型：</p>
<ul>
<li><code>char</code></li>
<li><code>int</code></li>
<li><code>short</code></li>
<li><code>long</code></li>
</ul>
<p>通常，你很可能会使用 <code>int</code> 保存整数。但是在某些情况下，你或许想在其它三个选项中选取合适的类型。</p>
<p><code>char</code> 类型通常被用来保存 ASCII 表中的字母，但是它也可以用来保存 <code>-128</code> 到 <code>127</code> 之间的小整数。它占据至少一个字节。</p>
<p><code>int</code> 占据至少两个字节。<code>short</code> 占据至少两个字节。<code>long</code> 占据至少四个字节。</p>
<p>如你所见，我们并不保证不同环境下的值相同。我们只有一个指示。问题在于每种数据类型中所存储的具体值是由实现和系统架构决定的。</p>
<p>我们保证 <code>short</code> 不会比 <code>int</code> 长。并且我们还保证 <code>long</code> 不会比 <code>int</code> 短。</p>
<p>ANSI C 规范标准确定了每种类型的最小值，多亏了它，我们至少可以知道使用某个类型时可以期待的最小值。</p>
<p>如果你正在 Arduino 上用 C 编程，不同的板子上的限制会有所不同。</p>
<p>在 Arduino Uno 开发板上，<code>int</code> 占两个字节，范围从 <code>-32,768</code> 到 <code>32,767</code>。在 Arduino MKR 1010 上，<code>int</code> 占四个字节，范围从 <code>-2,147,483,648</code> 到 <code>2,147,483,647</code>。差异还真不小。</p>
<p>在所有的 Arduino 开发板上，<code>short</code> 都占两个字节，范围从 <code>-32,768</code> 到 <code>32,767</code>。<code>long</code> 占四个字节，范围从 <code>-2,147,483,648</code>  到  <code>2,147,483,647</code>。</p>
<h3 id="">无符号整数</h3>
<p>对于以上所有的数据类型，我们都可以在其前面追加一个 <code>unsigned</code>。这样一来，值的范围就不再从负数开始，而是从 0 开始。这在很多情况下是很有用的。</p>
<ul>
<li><code>unsigned char</code> 的范围从 <code>0</code> 开始，至少到 <code>255</code></li>
<li><code>unsigned int</code> 的范围从 <code>0</code> 开始，至少到 <code>65,535</code></li>
<li><code>unsigned short</code> 的范围从 <code>0</code> 开始，至少到 <code>65,535</code></li>
<li><code>unsigned long</code> 的范围从 <code>0</code> 开始，至少到 <code>4,294,967,295</code></li>
</ul>
<h3 id="">溢出的问题</h3>
<p>鉴于所有这些限制，可能会出现一个问题：我们如何确保数字不超过限制？如果超过了限制会怎样？</p>
<p>如果你有一个值为 255 的 <code>unsigned int</code>，自增返回的值为 256，这在意料之中。如果你有一个值为 255 的 <code>unsigned char</code>，你得到的结果就是 0。它重置为了初始值。</p>
<p>如果你有一个值为 255 的 <code>unsigned char</code>，给它加上 10 会得到数字 <code>9</code>：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  unsigned char j = 255;
  j = j + 10;
  printf("%u", j); /* 9 */
}
</code></pre>
<blockquote>
<p>If you don't have a signed value, the behavior is undefined.<br>
原文这里可能是 typo，从代码来看，这里描述的是有符号整数的溢出行为。</p>
</blockquote>
<p>如果你的值是有符号的，程序的行为则是未知的。程序基本上会给你一个很大的值，这个值可能变化，就像这样：</p>
<pre><code class="language-c">include &lt;stdio.h&gt;

int main(void) {
  char j = 127;
  j = j + 10;
  printf("%u", j); /* 4294967177 */
}
</code></pre>
<p>换句话说，C 并不会在你超出类型的限制时保护你。对于这种情况，你需要自己当心。</p>
<h3 id="">声明错误类型时的警告</h3>
<p>如果你声明变量并用错误的值进行初始化，<code>gcc</code> 编译器（你可能正在使用这个编译器）应该会发出警告：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  char j = 1000;
}
</code></pre>
<pre><code>hello.c:4:11: warning: implicit conversion 
  from 'int' to
      'char' changes value from 1000 to -24
      [-Wconstant-conversion]
        char j = 1000;
             ~   ^~
1 warning generated.

</code></pre>
<p>如果你直接赋值，也会有警告：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  char j;
  j = 1000;
}

</code></pre>
<p>但是对值进行增加操作（例如，使用 <code>+=</code>）就不会有警告：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  char j = 0;
  j += 1000;
}
</code></pre>
<h3 id="">浮点数</h3>
<p>浮点类型可以表示的数值范围比整数大得多，还可以表示整数无法表示的分数。</p>
<p>使用浮点数时，我们将数表示成小数乘以 10 的幂。</p>
<p>你可能见过浮点数被写成</p>
<ul>
<li><code>1.29e-3</code></li>
<li><code>-2.3e+5</code></li>
</ul>
<p>和其它的一些看起来很奇怪的形式。</p>
<p>下面的几种类型：</p>
<ul>
<li><code>float</code></li>
<li><code>double</code></li>
<li><code>long double</code></li>
</ul>
<p>是用来表示带有小数点的数字（浮点类型）的。这几种类型都可以表示正数和负数。</p>
<p>任何 C 的实现都必须满足的最小要求是 <code>float</code> 可以表示范围在 10^-37 到 10^+37 之间的数，这通常用 32 位比特实现。 <code>double</code> 可以表示一组更大范围的数，<code>long double</code> 可以保存的数还要更多。</p>
<p>与整数一样，浮点数的确切值取决于具体实现。</p>
<p>在现代的 Mac 上，<code>float</code> 用 32 位表示，精度为 24 个有效位，剩余 8 位被用来编码指数部分。</p>
<p><code>double</code> 用 64 位表示，精度为 53 个有效位，剩余 11 为用于编码指数部分。</p>
<p><code>long double</code> 类型用 80 位表示，精度为 64 位有效位，剩余 15 位被用来编码指数部分。</p>
<p>你如何能在自己的计算机上确定这些类型的大小呢？你可以写一个程序来干这事儿：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  printf("char size: %lu bytes\n", sizeof(char));
  printf("int size: %lu bytes\n", sizeof(int));
  printf("short size: %lu bytes\n", sizeof(short));
  printf("long size: %lu bytes\n", sizeof(long));
  printf("float size: %lu bytes\n", sizeof(float));
  printf("double size: %lu bytes\n", sizeof(double));
  printf("long double size: %lu bytes\n", sizeof(long double));
}
</code></pre>
<p>在我的系统上（一台现代 Mac），输出如下：</p>
<pre><code>char size: 1 bytes
int size: 4 bytes
short size: 2 bytes
long size: 8 bytes
float size: 4 bytes
double size: 8 bytes
long double size: 16 bytes

</code></pre>
<h2 id="constants">常量</h2>
<p>咱们现在来谈谈常量。</p>
<p>常量的声明与变量类似，不同之处在于常量声明的前面带有 <code>const</code> 关键字，并且你总是需要给常量指定一个值。</p>
<p>就像这样：</p>
<pre><code class="language-c">const int age = 37;

</code></pre>
<p>这在 C 中是完全有效的，尽管通常情况下将常量声明为大写，就像这样：</p>
<pre><code class="language-c">const int AGE = 37;

</code></pre>
<p>虽然这只是一个惯例，但是在你阅读或编写 C 程序时，他能给你提供巨大的帮助，因为它提高了可读性。大写的名字意味着常量，小写的名字意味着变量。</p>
<p>常量的命名规则与变量相同：可以包含任意大小写字母、数字和下划线，但是不能以数字开头。<code>AGE</code> 和 <code>Age10</code> 都是有效的变量名，而 <code>1AGE</code> 就不是了。</p>
<p>另一种定义常量的方式是使用这种语法：</p>
<pre><code class="language-c">#define AGE 37

</code></pre>
<p>在这种情况下，你不需要添加类型，也不需要使用等于符号 <code>=</code>，并且可以省略末尾的分号。</p>
<p>C 编译器将会在编译时从声明的值推断出相应的类型。</p>
<h2 id="operators">运算符</h2>
<p>C 给我们提供了各种各样的运算符，我们可以用来操作数据。</p>
<p>特别地，我们可以识别不同分组的运算符：</p>
<ul>
<li>算术运算符</li>
<li>比较运算符</li>
<li>逻辑运算符</li>
<li>复合赋值运算符</li>
<li>位运算符</li>
<li>指针运算符</li>
<li>结构运算符</li>
<li>混合运算符</li>
</ul>
<p>在这一节中，我们将用两个假想的变量 <code>a</code> 和 <code>b</code> 举例，详细介绍所有这些运算符。</p>
<p>为了简单起见，我将不会介绍位运算符、结构运算符和指针运算符。</p>
<h3 id="">算术运算符</h3>
<p>我将把这个小型分组分为二元运算符和一元运算符。</p>
<p>二元操作符需要两个操作数：</p>
<table>
<thead>
<tr>
<th>操作符</th>
<th>名字</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>=</code></td>
<td>赋值</td>
<td><code>a = b</code></td>
</tr>
<tr>
<td><code>+</code></td>
<td>加</td>
<td><code>a + b</code></td>
</tr>
<tr>
<td><code>-</code></td>
<td>减</td>
<td><code>a - b</code></td>
</tr>
<tr>
<td><code>*</code></td>
<td>乘</td>
<td><code>a * b</code></td>
</tr>
<tr>
<td><code>/</code></td>
<td>除</td>
<td><code>a / b</code></td>
</tr>
<tr>
<td><code>%</code></td>
<td>取模</td>
<td><code>a % b</code></td>
</tr>
</tbody>
</table>
<p>一元运算符只需要一个操作数：</p>
<table>
<thead>
<tr>
<th>运算符</th>
<th>名字</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>+</code></td>
<td>一元加</td>
<td><code>+a</code></td>
</tr>
<tr>
<td><code>-</code></td>
<td>一元减</td>
<td><code>-a</code></td>
</tr>
<tr>
<td><code>++</code></td>
<td>自增</td>
<td><code>a++</code>  or  <code>++a</code></td>
</tr>
<tr>
<td><code>--</code></td>
<td>自减</td>
<td><code>a--</code>  or  <code>--a</code></td>
</tr>
</tbody>
</table>
<p><code>a++</code> 与 <code>++a</code> 的区别在于：<code>a++</code> 在使用 <code>a</code> 之后才自增它的值，而 <code>++a</code> 会在使用 <code>a</code> 之前自增它的值。</p>
<p>例如：</p>
<pre><code class="language-c">int a = 2;
int b;
b = a++ /* b 为 2，a 为 3 */
b = ++a /* b 为 4，a 为 4 */

</code></pre>
<p>这也适用于递减运算符。</p>
<h3 id="">比较运算符</h3>
<table>
<thead>
<tr>
<th>运算符</th>
<th>名字</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>==</code></td>
<td>相等</td>
<td><code>a == b</code></td>
</tr>
<tr>
<td><code>!=</code></td>
<td>不相等</td>
<td><code>a != b</code></td>
</tr>
<tr>
<td><code>&gt;</code></td>
<td>大于</td>
<td><code>a &gt; b</code></td>
</tr>
<tr>
<td><code>&lt;</code></td>
<td>小于</td>
<td><code>a &lt; b</code></td>
</tr>
<tr>
<td><code>&gt;=</code></td>
<td>大于等于</td>
<td><code>a &gt;= b</code></td>
</tr>
<tr>
<td><code>&lt;=</code></td>
<td>小于等于</td>
<td><code>a &lt;= b</code></td>
</tr>
</tbody>
</table>
<h3 id="">逻辑运算符</h3>
<ul>
<li><code>!</code> 非（例如：<code>!a</code>）</li>
<li><code>&amp;&amp;</code> 与（例如：<code>a &amp;&amp; b</code>）</li>
<li><code>||</code> 或（例如：<code>a || b</code>）</li>
</ul>
<p>这些运算符在使用布尔值时非常有用。</p>
<h3 id="">复合赋值运算符</h3>
<p>当赋值与算术运算同时进行时，这些运算符非常有用。</p>
<table>
<thead>
<tr>
<th>运算符</th>
<th>名字</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>+=</code></td>
<td>加且赋值</td>
<td><code>a += b</code></td>
</tr>
<tr>
<td><code>-=</code></td>
<td>减且赋值</td>
<td><code>a -= b</code></td>
</tr>
<tr>
<td><code>*=</code></td>
<td>乘且赋值</td>
<td><code>a *= b</code></td>
</tr>
<tr>
<td><code>/=</code></td>
<td>除且赋值</td>
<td><code>a /= b</code></td>
</tr>
<tr>
<td><code>%=</code></td>
<td>求模且赋值</td>
<td><code>a %= b</code></td>
</tr>
</tbody>
</table>
<h3 id="">三目运算符</h3>
<p>三目运算符是 C 中唯一一个使用三个操作数的运算符，并且它是表达条件的简便方法。</p>
<p>它看起来长这样：</p>
<p><code>&lt;条件&gt; ? &lt;表达式&gt; : &lt;表达式&gt;</code></p>
<p>示例：</p>
<p>若 <code>a</code> 的值为 <code>true</code>，就执行语句 <code>b</code>，否则执行语句 <code>c</code>。</p>
<p>三目运算符的功能与 if/else 条件语句相同，但是它更短，还可以被内联进表达式。</p>
<h3 id="sizeof">sizeof</h3>
<p><code>sizeof</code> 运算符返回你传入的操作数的大小。你可以传入变量，或者甚至是类型也可以。</p>
<p>使用示例：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  int age = 37;
  printf("%ld\n", sizeof(age));
  printf("%ld", sizeof(int));
}
</code></pre>
<h3 id="">运算符优先级</h3>
<p>对于所有的这些运算符（以及我们还没有在本文中介绍的其它运算符，包括位运算符、结构运算符和指针运算符），我们在单个表达式中一起使用它们时必须要留意。</p>
<p>假如我们有这个运算：</p>
<pre><code class="language-c">int a = 2;
int b = 4;
int c = b + a * a / b - a;
</code></pre>
<p><code>c</code> 的值是多少？我们在执行乘和除之前有进行加法操作吗？</p>
<p>这里是给我们解惑的一组规则。</p>
<p>按照顺序，优先级从低到高：</p>
<ul>
<li>赋值运算符 <code>=</code></li>
<li>二元运算符 <code>+</code> 和 <code>-</code></li>
<li>运算符 <code>*</code> 和 <code>/</code></li>
<li>一元运算符 <code>+</code> 和 <code>-</code></li>
</ul>
<p>运算符还具有关联规则，除了一元运算符和赋值运算符之外，该规则总是从左到右的。</p>
<p>在：</p>
<pre><code class="language-c">int c = b + a * a / b - a;
</code></pre>
<p>中，我们首先执行 <code>a * a / b</code>，由于是从左到右的，我们可以拆分为 <code>a * a</code> 与其结果 <code>/b</code>：<code>2 * 2 = 4</code>，<code>4 / 4 = 1</code>。</p>
<p>然后我们可以进行加法操作和减法操作：4 + 1 - 2。<code>c</code> 的值是 <code>3</code>。</p>
<p>然而，在所有的示例中，我都想确保你意识到你可以使用括号让任何相似的表达式更易读和易理解。</p>
<p>括号的优先级比其它任何运算符都要高。</p>
<p>上述示例表达式可以被重写为：</p>
<pre><code class="language-c">int c = b + ((a * a) / b) - a;
</code></pre>
<p>并且我们不必考虑太多。</p>
<h2 id="conditionals">条件语句</h2>
<p>任何编程语言都给程序员提供了进行选择的能力。</p>
<p>我们想要在一些情况下进行 X，而在其它情况下进行 Y。</p>
<p>我们想检查数据，根据数据的状态做选择。</p>
<p>C 给我们提供了两种方式。</p>
<p>第一种方式是带 <code>else</code> 的 <code>if</code> 语句，第二种是 <code>switch</code> 语句。</p>
<h3 id="if">if</h3>
<p>在 <code>if</code> 语句中，你可以在检查到条件为 true 的时候，执行花括号内的代码块：</p>
<pre><code class="language-c">int a = 1;

if (a == 1) {
  /* 进行一些操作 */
}
</code></pre>
<p>如果原始条件的结果是 false，你可以追加一个 <code>else</code> 块以不同的代码块：</p>
<pre><code class="language-c">int a = 1;

if (a == 2) {
  /* 进行一些操作 */
} else {
  /* 进行另一些操作 */
}
</code></pre>
<p>谨防一种常见的缺陷源——总是在比较中使用比较运算符 <code>==</code>，而不是赋值运算符 <code>=</code>。如果你不这么做，除非参数为 <code>0</code>，否则 <code>if</code> 条件检查的结果将一直都是 true。例如，如果你这么做：</p>
<pre><code class="language-c">int a = 0;

if (a = 0) {
  /* 永远都不会被调用 */
}
</code></pre>
<p>为什么会这样呢？因为条件检查会寻找一个布尔类型的结果（比较的结果），数字 <code>0</code> 总是等于 false。其它的任何东西都是 true，包括负数。</p>
<p>通过将多个 <code>if</code> 语句堆叠在一起，你可以有多个 <code>else</code> 块：</p>
<pre><code class="language-c">int a = 1;

if (a == 2) {
  /* do something */
} else if (a == 1) {
  /* 进行一些操作 */
} else {
  /* 进行另一些操作 */
}
</code></pre>
<h3 id="switch">switch</h3>
<p>如果你的检查需要使用非常多的 if/else/if 块，可能是因为你需要检查变量的具体值，这时 <code>switch</code> 语句对你来说就非常有用了。</p>
<p>你可以提供一个变量作为条件，然后为期望的每个值使用一个 <code>case</code> 入口点：</p>
<pre><code class="language-c">int a = 1;

switch (a) {
  case 0:
    /* 进行一些操作 */
    break;
  case 1:
    /* 进行另一些操作 */
    break;
  case 2:
    /* 进行另一些操作 */
    break;
</code></pre>
<p>当前一个 case 执行完后，为了避免下一个 case 被执行，我们需要在每个 case 的末尾使用 <code>break</code> 关键字。这种“级联”效果在某些创造性方法中非常有用的。</p>
<p>你可以在末尾添加一个“捕获所有的” case，名为 <code>default</code>：</p>
<pre><code class="language-c">int a = 1;

switch (a) {
  case 0:
    /* 进行一些操作 */
    break;
  case 1:
    /* 进行另一些操作 */
    break;
  case 2:
    /* 进行另一些操作 */
    break;
  default:
    /* 处理所有其它的情况 */
    break;
}
</code></pre>
<h2 id="loops">循环</h2>
<p>C 给我们提供了三种循环：<strong>For 循环</strong>、<strong>while 循环</strong> 和 <strong>do while 循环</strong>。它们都允许你在数组上进行迭代，但又各有不同。咱们仔细来看一看它们。</p>
<h3 id="for">For 循环</h3>
<p>第一种执行循环是 <strong>for 循环</strong>，它可能也是最常见的循环。</p>
<p>使用 <code>for</code> 关键字时，我们可以先定义循环的 <em>规则</em>，然后提供反复执行的那个代码块。</p>
<p>就像这样：</p>
<pre><code class="language-c">for (int i = 0; i &lt;= 10; i++) {
  /* 反复执行的指令 */
}
</code></pre>
<p><code>(int i = 0; i &lt;= 10; i++)</code> 代码块包含与循环细节有关的三个部分：</p>
<ul>
<li>初始条件（<code>int i = 0</code>）</li>
<li>测试（<code>i &lt;= 10</code>）</li>
<li>增长（<code>i++</code>）</li>
</ul>
<p>我们首先定义循环变量，本示例中为 <code>i</code>。<code>i</code> 是循环中的一个常用变量名，<code>j</code> 是嵌套循环（循环内的循环）内使用的变量名。这只是一个惯例。</p>
<p>变量 <code>i</code> 的值被初始化为 0，并且第一次迭代执行完毕。然后 <code>i</code> 像增长部分（这个示例中是 <code>i++</code>，递增 1）所说的那样增长，并且所有的循环会一直重复，直到 <code>i</code> 的值达到数字 10。</p>
<p>在循环的主代码块内，我们可以访问变量 <code>i</code>，从而获知我们当前所处的是哪个迭代。这个程序应该打印 <code>0 1 2 3 4 5 5 6 7 8 9 10</code>：</p>
<pre><code class="language-c">for (int i = 0; i &lt;= 10; i++) {
  /* 反复执行的指令 */
  printf("%u ", i);
}
</code></pre>
<p>循环可以从较高的数字开始，往较低的数字逼近，就像这样：</p>
<pre><code class="language-c">for (int i = 10; i &gt; 0; i--) {
  /* 反复执行的指令 */
}
</code></pre>
<p>你也可以让循环变量的增量为 2 或者其它值：</p>
<pre><code class="language-c">for (int i = 0; i &lt; 1000; i = i + 30) {
  /* 反复执行的指令 */
}
</code></pre>
<h3 id="while">while 循环</h3>
<p><strong>while 循环</strong> 写起来比 <code>for</code> 循环要简单，因为它需要你在自己的部分做更多的事情。</p>
<p>使用 <code>while</code> 时，你只需要检查条件，而不用在循环开始时预先定义所有的循环数据（就像你在 <code>for</code> 循环中做的那样）：</p>
<pre><code class="language-c">while (i &lt; 10) {

}
</code></pre>
<p>这段代码假定 <code>i</code> 已经定义并且用某个值进行了初始化。</p>
<p>除非你在循环内的某些地方增加变量 <code>i</code> 的值，否则这个循环会变成一个 <strong>无限循环</strong>。无限循环非常糟糕，因为它会阻塞程序，从而使其它任何事情都不会发生。</p>
<p>对于一个“正确的” while 循环，这是你需要知道的：</p>
<pre><code class="language-c">int i = 0;

while (i &lt; 10) {
  /* 做点事情 */

  i++;
}
</code></pre>
<p>其中有一个例外，我们将会在一分钟后看到它。在这之前，让我介绍下 <code>do while</code>。</p>
<h3 id="dowhile">Do while 循环</h3>
<p>while 循环非常棒，但是有些时候你可能需要做某件特定的事情：你总是想执行某个代码块，然后 <em>可能</em> 一直重复它。</p>
<p>这可以通过 <code>do while</code> 关键字来完成。它在某种程度上和 <code>while</code> 循环非常类似，但是会有些许不同：</p>
<pre><code class="language-c">int i = 0;

do {
  /* 做点事情 */

  i++;
} while (i &lt; 10);
</code></pre>
<p>尽管条件检查在底部，但是包含注释 <code>/* 做点事情 */</code> 的代码块总是会至少执行一次。</p>
<p>然后，只要 <code>i</code> 小于 10，我们都将会重复这个代码块。</p>
<h3 id="break">使用 break 跳出循环</h3>
<p>在所有的 C 循环内，不管循环的条件设置得如何，我们都有一种在某个时间立即跳出循环的方法。</p>
<p>这是通过 <code>break</code> 关键字来完成的。</p>
<p>这在很多情况下非常有用，你可能想检查某个变量的值，例如：</p>
<pre><code class="language-c">for (int i = 0; i &lt;= 10; i++) {
  if (i == 4 &amp;&amp; someVariable == 10) {
    break;
  }
}
</code></pre>
<p>对 <code>while</code> 循环（也适用于 <code>do while</code> 循环）来说，使用这种方式跳出循环非常有趣，因为我们可以创建一个看似无限的循环，不过我们可以在某个条件发生时结束这个循环。你可以在循环代码块里面定义它：</p>
<pre><code class="language-c">int i = 0;
while (1) {
  /* 做点事情 */

  i++;
  if (i == 10) break;
}
</code></pre>
<p>这种循环在 C 中非常普遍。</p>
<h2 id="arrays">数组</h2>
<p>数组是存储多个变量的变量。</p>
<p>在 C 中，数组中的每个值都必须有 <strong>相同的类型</strong>。这意味着你将会有 <code>int</code> 值组成的数组， <code>double</code> 值组成的数组，等等。</p>
<p>你可以像这样定义一个 <code>int</code> 型的数组：</p>
<pre><code class="language-c">int prices[5];
</code></pre>
<p>你必须总是声明数组的大小。C 没有提供开箱即用的动态数组（为此，你必须使用像链表这样的数据结构）。</p>
<p>你可以使用常量定义数组的大小：</p>
<pre><code class="language-c">const int SIZE = 5;
int prices[SIZE];
</code></pre>
<p>你可以在定义数组的时候进行初始化，就像这样：</p>
<pre><code class="language-c">int prices[5] = { 1, 2, 3, 4, 5 };
</code></pre>
<p>但是你也可以在定义数组之后为其赋值，用这种方式：</p>
<pre><code class="language-c">int prices[5];

prices[0] = 1;
prices[1] = 2;
prices[2] = 3;
prices[3] = 4;
prices[4] = 5;
</code></pre>
<p>或者使用循环，这更加实际：</p>
<pre><code class="language-c">int prices[5];

for (int i = 0; i &lt; 5; i++) {
  prices[i] = i + 1;
}
</code></pre>
<pre><code class="language-c">prices[0]; /* 第一个数组项的值 */
prices[1]; /* 第二个数组项的值 */
</code></pre>
<p>数组的索引从 0 开始，所以一个有五个元素的数组，比如上面的 <code>prices</code> 数组，将会包含的数组项的范围为 <code>prices[0]</code> 到 <code>prices[4]</code>。</p>
<p>有趣的是，C 数组中的所有元素都是顺序存放的，一个接一个。高级编程语言通常不会出现这种情况。</p>
<p>另一件有趣的事情是：数组的变量名，上述示例中的 <code>prices</code>，是一个指向数组中首个元素的 <strong>指针</strong>。因此，可以像普通指针一样使用数组。</p>
<p>稍后会介绍更多有关指针的内容。</p>
<h2 id="strings">字符串</h2>
<p>在 C 中，字符串是一种特殊的数组：字符串是由 <code>char</code> 值组成的数组：</p>
<pre><code class="language-c">char name[7];
</code></pre>
<p>我在介绍 C 中的数据类型时介绍过 <code>char</code> 类型，但是简而言之，它通常用于存储 ASCII 表中的字母。</p>
<p>可以像初始化一个普通的数组那样初始化一个字符串：</p>
<pre><code class="language-c">char name[7] = { "F", "l", "a", "v", "i", "o" };
</code></pre>
<p>或者使用更加方便的字符串字面量（也被称为字符串常量），一组用双引号引起来的字符：</p>
<pre><code class="language-c">char name[7] = "Flavio";
</code></pre>
<p>你可以通过 <code>printf()</code> 打印字符串，使用 <code>%s</code>：</p>
<pre><code class="language-c">printf("%s", name);
</code></pre>
<p>你有注意到“Flavio”是 6 个字符长，但是我定义了一个长度为 7 的数组吗？这是因为字符串中的最后一个字符必须是 <code>0</code>，它是字符串的终止符号，我们必须给它留个位置。</p>
<p>记住这个非常重要，尤其是当你操作字符串的时候。</p>
<p>说到操作字符串，C 提供了一个非常重要的标准库：<code>string.h</code>。</p>
<p>这个库是必不可少的，因为它抽象了很多与字符串有关的底层细节，给我们提供了一组非常有用的函数。</p>
<p>你可以在程序中加载这个库，需要在文件顶部加上：</p>
<pre><code class="language-c">#include &lt;string.h&gt;
</code></pre>
<p>一旦你这么做了之后，你就可以访问函数：</p>
<ul>
<li><code>strcpy()</code>：将一个字符串复制到另一个字符串</li>
<li><code>strcat()</code>：将一个字符串追加到另一个字符串</li>
<li><code>strcmp()</code>：比较两个字符串是否相等</li>
<li><code>strncmp()</code>：比较两个字符串的前 <code>n</code> 个字符</li>
<li><code>strlen()</code>：计算字符串的长度</li>
</ul>
<p>还有很多很多其它的函数供你调用。</p>
<h2 id="pointers">指针</h2>
<p>在我看来，指针是 C 中最令人不解/最具挑战的部分。尤其当你是编程新手的时候，如果你是从像 Python 或 JavaScript 这样的高级语言来到 C 的，也会这样。</p>
<p>在这一节中，我想以最简单但又不模糊的方式介绍它们。</p>
<p>指针是某个内存块的地址，这个内存块包含一个变量。</p>
<p>当你像这样声明一个整数时：</p>
<pre><code class="language-c">int age = 37;
</code></pre>
<p>我们可以使用 <code>&amp;</code> 运算符获取内存中该变量的地址值：</p>
<pre><code class="language-c">printf("%p", &amp;age); /* 0x7ffeef7dcb9c */
</code></pre>
<p>我在 <code>printf()</code> 内声明 <code>%p</code> 格式来打印地址值。</p>
<p>我们可以将该地址赋给一个变量：</p>
<pre><code class="language-c">int address = &amp;age;
</code></pre>
<p>当在声明中使用 <code>int *address</code> 时，我们并没有在声明一个整数值，而是在声明一个 <strong>指向一个整数的指针</strong>。</p>
<p>我们可以使用指针运算符获取该地址指向的变量的值：</p>
<pre><code class="language-c">int age = 37;
int *address = &amp;age;
printf("%u", *address); /* 37 */
</code></pre>
<p>我们又一次使用指针运算符，但是由于这次它不是一个声明，所以它表示“该指针指向的变量的值”。</p>
<p>在这个示例中，我们声明了一个 <code>age</code> 变量，但是我们使用了一个指针来初始化它的值：</p>
<pre><code class="language-c">int age;
int *address = &amp;age;
*address = 37;
printf("%u", *address);
</code></pre>
<p>在使用 C 时，你会发现很多东西都建立在这个简单的概念之上。所以自己运行一下上面的示例，确保你对它有所熟悉。</p>
<p>指针是一个非常好的机会，因为它们迫使我们考虑内存地址以及数据是如何组织的。</p>
<p>数组就是一个例子。当你声明一个数组时：</p>
<pre><code class="language-c">int prices[3] = { 5, 4, 3 };
</code></pre>
<p><code>prices</code> 变量实际上是一个指向数组首个元素的指针。在这种情况下，你可以使用这个 <code>printf()</code> 函数获取第一个数组元素的值：</p>
<pre><code class="language-c">printf("%u", *prices); /* 5 */
</code></pre>
<p>我们可以通过给 <code>prices</code> 指针加一来获取第二个元素，这是一件非常酷的事情：</p>
<pre><code class="language-c">printf("%u",`_ `(prices + 1)); /* 4 */
</code></pre>
<p>这种做法对于所有的其它值也适用。</p>
<p>我们还可以进行很多非常美妙的字符串操作，因为字符串的底层就是数组。</p>
<p>我们还有很多其它的使用场景，包括传递对象或函数的引用，从而避免消耗更多的资源来进行复制。</p>
<h2 id="functions">函数</h2>
<p>我们通过函数将代码组织成子例程，这样就可以：</p>
<ol>
<li>给它一个名字</li>
<li>在需要它们的时候进行调用</li>
</ol>
<p>从你的第一个程序（“Hello, World!”）开始，你就在使用 C 函数了：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
    printf("Hello, World!");
}
</code></pre>
<p><code>main()</code> 函数是一个非常重要的函数，它是 C 程序的入口点。</p>
<p>这是另一个函数：</p>
<pre><code class="language-c">void doSomething(int value) {
    printf("%u", value);
}
</code></pre>
<p>函数有 4 个重要的方面：</p>
<ol>
<li>它们有一个名字，所以我们可以在之后调用它们</li>
<li>它们声明一个返回值</li>
<li>它们可以有参数</li>
<li>它们有一个函数体，用花括号包裹</li>
</ol>
<p>函数体是一组指令，任何时候，只要函数被调用，这组指令就会被执行。</p>
<p>如果函数没有返回值，你可以在函数名前面使用关键字 <code>void</code>。否则你就要声明该函数的返回值类型（整数为 <code>int</code>，浮点数为 <code>float</code>，字符串为 <code>const char *</code>，等等）。</p>
<p>函数返回值的数量不能超过一个。</p>
<p>函数可以有参数。它们是可选的。如果函数没有参数，我们就在括号内插入 <code>void</code>，就像这样：</p>
<pre><code class="language-c">void doSomething(void) {
  /* ... */
}
</code></pre>
<p>在这种情况下，当我们调用该函数时，括号内没有任何东西：</p>
<pre><code class="language-c">doSomething();
</code></pre>
<p>如果有一个参数，我们就声明该参数的类型和名字，就像这样：</p>
<pre><code class="language-c">void doSomething(int value) {
   /* ... */
}
</code></pre>
<p>当我们调用该函数时，我们会在括号内传递对应的参数，就像这样：</p>
<pre><code class="language-c">doSomething(3);
</code></pre>
<p>我们可以有多个参数，为此我们使用逗号对它们进行分隔，在声明和调用时都是这样：</p>
<pre><code class="language-c">void doSomething(int value1, int value2) {
   /* ... */
}

doSomething(3, 4);
</code></pre>
<p>参数是通过 <strong>拷贝</strong> 传递的。这意味着如果你修改 <code>value1</code>，它的值是在局部作用域内修改的。函数外的那个值，即我们在调用时传入的值，并不会改变。</p>
<p>如果你传入的参数为一个 <strong>指针</strong>，你可以修改该变量的值，因为你现在可以使用它的内存地址直接访问它。</p>
<p>你不能为参数定义默认值。C++ 是可以的（Arduino Language 程序也可以），但是 C 不行。</p>
<p>确保你在调用函数之前定义了该函数，否则编译器将会给出一个警告和一个错误：</p>
<pre><code>➜  ~ gcc hello.c -o hello; ./hello
hello.c:13:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
hello.c:17:6: error: conflicting types for
      'doSomething'
void doSomething(int value1, char value2) {
     ^
hello.c:13:3: note: previous implicit declaration
      is here
  doSomething(3, 4);
  ^
1 warning and 1 error generated.
</code></pre>
<p>你收到的警告与顺序有关，我之前有提到过这个。</p>
<p>错误与另一件事情有关。因为 C 没有在调用函数之前没有“看到”该函数的声明，所以它必须进行假设。并且，它假设该函数返回 <code>int</code>。然而该函数返回的是 <code>void</code>，因此出现了错误。</p>
<p>如果你将该函数的定义修改为：</p>
<pre><code class="language-c">int doSomething(int value1, int value2) {
  printf("%d %d\n", value1, value2);
  return 1;
}
</code></pre>
<p>你就只会得到警告，错误消失了：</p>
<pre><code>➜  ~ gcc hello.c -o hello; ./hello
hello.c:14:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
1 warning generated.
</code></pre>
<p>不管是何种情况，确保你在使用函数之前声明了它。要么将函数上移，要么在头文件中加入该函数的原型。</p>
<p>在函数内部，你可以声明变量：</p>
<pre><code class="language-c">void doSomething(int value) {
  int doubleValue = value * 2;
}
</code></pre>
<p>变量在调用该函数的那一刻创建，并且在函数退出的时候销毁。它对函数外面来说是不可见的。</p>
<p>在函数内部，你可以调用函数自己。这被称为 <strong>递归</strong>，它提供了特有的机会。</p>
<h2 id="input-and-output">输入与输出</h2>
<p>C 是一门小型语言，并且 C 的“内核”并不包含任何输入/输出（I/O）功能。</p>
<p>当然，这并不是 C 所独有的。语言内核与 I/O 无关是很常见的。</p>
<p>在 C 中，输入/输出由 C 的标准库通过一组定义在 <code>stdio.h</code> 头文件中的函数向我们提供。</p>
<p>你可以在 C 文件顶部使用：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
</code></pre>
<p>导入这个库。</p>
<p>这个库给我们提供了很多其它的函数：</p>
<ul>
<li><code>printf()</code></li>
<li><code>scanf()</code></li>
<li><code>sscanf()</code></li>
<li><code>fgets()</code></li>
<li><code>fprintf()</code></li>
</ul>
<p>在描述这个函数干啥之前，我想先花一分钟讲一下 <strong>I/O 流</strong>。</p>
<p>在 C 中，我们有三种类型的 I/O 流：</p>
<ul>
<li><code>stdin</code>（标准输入）</li>
<li><code>stdout</code>（标准输出）</li>
<li><code>stderr</code>（标准错误）</li>
</ul>
<p>借助 I/O 函数，我们始终可以和流一起工作。流是一个高级接口，可以代表一个设备或文件。从 C 的角度来看，我们在从文件读取和命令行读取没有任何差异：不论如何，它都是一个 I/O 流。</p>
<p>那是我们需要牢记的一件事情。</p>
<p>某些函数是为与特定的流一起工作而设计的，就像 <code>printf()</code>一样，我们用它来将字符串打印到 <code>stdout</code>。使用它更加通用的版本 <code>fprintf()</code> 时，我们可以指定我们要写到的流。</p>
<p>由于我最开始谈论的是 <code>printf()</code>，咱们现在就介绍它吧。</p>
<p><code>printf()</code> 是你在学习 C 编程时最先使用的函数之一。</p>
<p>在它最简单的使用形式中，你给它传递一个字符串字面量：</p>
<pre><code class="language-c">printf("hey!");
</code></pre>
<p>并且程序会将该字符串的内容打印到屏幕上。</p>
<p>你可以打印一个变量的值。但是这有点棘手，因为你需要添加一个特殊的字符，一个占位符，它会根据变量的类型变化。例如，我们为有符号十进制整数使用 <code>%d</code>：</p>
<pre><code class="language-c">int age = 37;

printf("My age is %d", age);
</code></pre>
<p>通过使用逗号，我现在可以打印多个变量：</p>
<pre><code class="language-c">int age_yesterday = 37;
int age_today = 36;

printf("Yesterday my age was %d and today is %d", age_yesterday, age_today);
</code></pre>
<p>还有其它像 <code>%d</code> 一样的格式指示符：</p>
<ul>
<li><code>%c</code> 用于字符</li>
<li><code>%s</code> 用于字符串</li>
<li><code>%f</code> 用于浮点数</li>
<li><code>%p</code> 用于指针</li>
</ul>
<p>还有很多。</p>
<p>我们可以在 <code>printf()</code> 中使用转义字符，比如 <code>\n</code> 可以用来让输出创建一个新行。</p>
<h3 id="scanf"><code>scanf()</code></h3>
<p><code>printf()</code> 被用作输出函数。我现在想介绍一个输入函数，这样我们就能完成所有的 I/O 操作：<code>scanf()</code>。</p>
<p>这个函数被用来从用户运行的程序，从命令行获取一个值。</p>
<p>我们必须先定义一个变量，它将被用来存放我们从输入中获取的值：</p>
<pre><code class="language-c">int age;
</code></pre>
<p>然后我们调用 <code>scanf()</code>，传入两个参数：变量的格式（类型），和变量的地址：</p>
<pre><code class="language-c">scanf("%d", &amp;age);
</code></pre>
<p>如果我们想在输入时获取一个字符串，还记得字符串名是一个指向第一个字符的指针，所以你不需要在它前面加上 <code>&amp;</code>：</p>
<pre><code class="language-c">char name[20];
scanf("%s", name);
</code></pre>
<p>这里是一个小程序，它同时使用了 <code>printf()</code> 和 <code>scanf()</code>：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  char name[20];
  printf("Enter your name: ");
  scanf("%s", name);
  printf("you entered %s", name);
}
</code></pre>
<h2 id="variable-scope">变量作用域</h2>
<p>当你在 C 程序中定义一个变量时，根据你声明它的位置，它会有一个不同的 <strong>作用域（scope）</strong>。</p>
<p>这意味着它将会在某些地方可用，而在其它地方不可用。</p>
<p>该位置决定了两种类型的变量：</p>
<ul>
<li><strong>全局变量（global variables）</strong></li>
<li><strong>局部变量（local variables）</strong></li>
</ul>
<p>这就是区别：在函数内部声明的变量就是局部变量，比如这个：</p>
<pre><code class="language-c">int main(void) {
  int age = 37;
}
</code></pre>
<p>局部变量只有在函数内才能访问，它们会在函数结束后不复存在。它们会被从内存中清除掉（有一些例外）。</p>
<p>定义在函数外部的变量就是全局变量，比如这个示例：</p>
<pre><code class="language-c">int age = 37;

int main(void) {
  /* ... */
}
</code></pre>
<p>全局变量可以从程序中的任何一个函数访问，它们在整个程序的执行过程中都是可用的，直到程序结束。</p>
<p>我提到过局部变量在函数结束之后就不再可用。</p>
<p>原因是局部变量默认是在 <strong>栈（stack）</strong> 上声明的，除非你使用指针在堆中显式地分配它们。但是这样一来，你就不得不自己管理内存了。</p>
<h2 id="static-variables">静态变量</h2>
<p>在函数内部，你可以使用 <code>static</code> 关键字初始化一个 <strong>静态变量（static variable）</strong>。</p>
<p>我说了“在函数内部”，因为全局变量默认就是静态的，所以没有必要再添加这个关键字。</p>
<p>什么是静态变量？静态变量在没有声明初始值的时候会被初始化为 0，并且它会在函数调用中保持该值。</p>
<p>考虑这个函数：</p>
<pre><code class="language-c">int incrementAge() {
  int age = 0;
  age++;
  return age;
}
</code></pre>
<p>如果我们调用一次 <code>incrementAge()</code>，我们将会得到返回值 <code>1</code>。如果我们再调用一次，我们总是会得到 1，因为 <code>age</code> 是一个局部变量并且在每次调用该函数的时候都会被重新初始化为 <code>0</code>。</p>
<p>如果我们将该函数改为：</p>
<pre><code class="language-c">int incrementAge() {
  static int age = 0;
  age++;
  return age;
}
</code></pre>
<p>现在我们每调用一次这个函数，我们就会得到一个增加了的值：</p>
<pre><code class="language-c">printf("%d\n", incrementAge());
printf("%d\n", incrementAge());
printf("%d\n", incrementAge());
</code></pre>
<p>将会给我们：</p>
<pre><code>1
2
3
</code></pre>
<p>我们也可以在 <code>static int age = 0;</code> 中省略初始化 <code>age</code> 为 0 的代码，只写 <code>static int age;</code>，因为静态变量在创建时会自动设置为 0。</p>
<p>我们也可以有静态数组。这时，每一个数组元素都被初始化为 0：</p>
<pre><code class="language-c">int incrementAge() {
  static int ages[3];
  ages[0]++;
  return ages[0];
}
</code></pre>
<h2 id="global-variables">全局变量</h2>
<p>在这一节中，我想多谈论一点 <strong>全局变量与局部变量</strong> 之间的差异。</p>
<p><strong>局部变量</strong> 被定义在函数内部，只在该函数内可用。</p>
<p>就像这样：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
  char j = 0;
  j += 10;
  printf("%u", j); //10
}
</code></pre>
<p><code>j</code> 在 <code>main</code> 函数之外的任何地方都不可用。</p>
<p><strong>全局变量</strong> 定义在所有函数的外部，就像这样：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

char i = 0;

int main(void) {
  i += 10;
  printf("%u", i); //10
}
</code></pre>
<p>全局变量可以被程序内的任何函数访问。该访问并不只局限于读取全局变量的值：任何函数都可以更新全局变量的值。</p>
<p>因此，全局变量是一种在函数间共享相同数据的一种方式。</p>
<p>局部变量的主要不同在于，分配给局部变量的内存会在函数结束之后立即释放。</p>
<p>全局变量只在程序结束时才会释放。</p>
<h2 id="type-definitions">类型定义</h2>
<p>C 中的 <code>typedef</code> 关键字允许你定义新的类型。</p>
<p>我们可以从 C 内置的类型开始创建自己的类型，使用这个语法：</p>
<pre><code class="language-c">typedef existingtype NEWTYPE
</code></pre>
<p>按照惯例，我们创建的新类型通常是大写的。</p>
<p>这样可以更加容易区分它，并且可以立即识别出它是一种类型。</p>
<p>例如，我们可以定义一个新的 <code>NUMBER</code> 类型，它还是 <code>int</code>：</p>
<pre><code class="language-c">typedef int NUMBER
</code></pre>
<p>一旦你这么做了之后，你就可以定义新的 <code>NUMBER</code> 变量了：</p>
<pre><code class="language-c">NUMBER one = 1;
</code></pre>
<p>现在你可能会问：为什么？为什么不直接使用内置的 <code>int</code> 类型呢？</p>
<p>嗯，当两个东西搭配在一起的时候，<code>typedef</code> 会变得真的很有用：枚举类型和结构体。</p>
<h2 id="enumerated-types">枚举类型</h2>
<p>使用 <code>typedef</code> 和 <code>enum</code> 关键字，我们可以定义具有指定值的类型。</p>
<p>这是 <code>typedef</code> 关键字最重要的使用场景之一。</p>
<p>这是枚举类型的语法：</p>
<pre><code class="language-c">typedef enum {
  //值……
}
</code></pre>
<p>按照惯例，我们创建的枚举类通常是大写的。</p>
<p>这里是一个简单的示例：</p>
<pre><code class="language-c">typedef enum {
  true,
  false
} BOOLEAN;
</code></pre>
<p>C 自带 <code>bool</code> 类型，所以这个示例并不实用，但是它会让你领悟到其中的精髓。</p>
<p>另一个示例是定义一周中的那几个日子：</p>
<pre><code class="language-c">typedef enum {
  monday,  
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
} WEEKDAY;
</code></pre>
<p>这里是使用这个枚举类的一个简单程序：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

typedef enum {
  monday,  
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
} WEEKDAY;

int main(void) {
  WEEKDAY day = monday;

  if (day == monday) {
    printf("It's monday!"); 
  } else {
    printf("It's not monday"); 
  }
}
</code></pre>
<p>枚举定义中的每个枚举项在内部都与一个整数配对。所以在这个示例中 <code>monday</code> 是 0，<code>tuesday</code> 是 1，以此类推。</p>
<p>这意味着对应的条件可以是 <code>if (day == 0)</code> 而不是 <code>if (day == monday)</code>，但是对于我们人类来说，使用名字比数字更合理，所以它是一个非常便利的语法。</p>
<h2 id="structures">结构体</h2>
<p>利用 <code>struct</code> 关键字，我们可以使用基本的 C 类型创建复杂的数据结构。</p>
<p>结构体是一组由不同类型的值组成的集合。C 中的数组被限制为一种类型，所以结构体在很多用例中会显得非常有趣。</p>
<p>这里是结构体的语法：</p>
<pre><code class="language-c">struct &lt;structname&gt; {
  //变量……
};
</code></pre>
<p>示例：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
};
</code></pre>
<p>通过将变量添加到右花括号之后，分号之前，你可以声明类型为该结构体的变量，就像这样：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
} flavio;
</code></pre>
<p>或者多个变量也行，就像这样：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
} flavio, people[20];
</code></pre>
<p>这次我声明一个名为 <code>flavio</code> 的 <code>person</code> 变量，以及一个具有 20 个 <code>person</code> 的名为 <code>people</code> 的数组。</p>
<p>我们也可以稍后再声明变量，使用这个语法：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
};

struct person flavio;
</code></pre>
<p>我们可以在声明的时候初始化一个结构体：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };
</code></pre>
<p>一旦定义了结构体，我们就可以使用一个点（<code>.</code>）来访问它里面的值了：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };
printf("%s, age %u", flavio.name, flavio.age);
</code></pre>
<p>我们也可以使用点语法改变结构体中的值：</p>
<pre><code class="language-c">struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };

flavio.age = 38;
</code></pre>
<p>结构体非常有用，因为它们既可以作为函数的参数，也可以作为函数的返回值，以及它们内部的嵌入变量。每个变量都有一个标签。</p>
<p>注意到结构体是 <strong>复制传递</strong> 的，这一点很重要，除非，当然你可以传递一个指向结构体的指针，这种情况下它就是引用传递。</p>
<p>使用 <code>typedef</code>，我们可以简化处理结构体时的代码。</p>
<p>咱们看一个示例：</p>
<pre><code class="language-c">typedef struct {
  int age;
  char *name;
} PERSON;
</code></pre>
<p>按照惯例，我们使用 <code>typedef</code> 创建的结构体通常是大写的。</p>
<p>现在，我们可以像这样声明一个新的 <code>PERSON</code> 变量：</p>
<pre><code class="language-c">PERSON flavio;
</code></pre>
<p>并且我们可以用这种方式在声明的时候初始化它们：</p>
<pre><code class="language-c">PERSON flavio = { 37, "Flavio" };
</code></pre>
<h2 id="command-line-parameters">命令行参数</h2>
<p>在 C 程序中，你可能需要在命令启动时从命令行接收参数。</p>
<p>对于简单的需求而言，你只需要将 <code>main()</code> 函数的签名从</p>
<pre><code class="language-c">int main(void)
</code></pre>
<p>修改为</p>
<pre><code class="language-c">int main (int argc, char *argv[])
</code></pre>
<p><code>argc</code> 是一个整数，包含从命令行提供的参数的数量。</p>
<p><code>argv</code> 是一个字符串数组。</p>
<p>当程序开始运行时，我们用这两个参数给主函数提供参数。</p>
<p>注意 <code>argv</code> 数组中总是至少有一个元素：程序的名字。</p>
<p>咱们以我们用来运行程序的 C 编译器作为示例吧，就像这样：</p>
<pre><code class="language-c">gcc hello.c -o hello
</code></pre>
<p>如果这就是我们的程序，我们的 <code>argc</code> 将会是 4，<code>argv</code> 将是一个包含以下内容的数组：</p>
<ul>
<li><code>gcc</code></li>
<li><code>hello.c</code></li>
<li><code>-o</code></li>
<li><code>hello</code></li>
</ul>
<p>咱们写一个打印它收到的参数的程序吧：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main (int argc, char *argv[]) {
  for (int i = 0; i &lt; argc; i++) {
    printf("%s\n", argv[i]);
  }
}
</code></pre>
<p>如果我们的程序名为 <code>hello</code>，并且我们像这样运行它：<code>./hello</code>，我们就会得到以下输出：</p>
<pre><code>./hello
</code></pre>
<p>如果我们传递一些随机参数，就像这样：<code>./hello a b c</code>，我们竟会在终端中得到这个输出：</p>
<pre><code>./hello
a
b
c
</code></pre>
<p>对于简单的需求而言，这个系统工作得很好。对于更加复杂的需求，有一些常用的包，比如 <strong>getopt</strong>。</p>
<h2 id="header-files">头文件</h2>
<p>简单的程序可以直接放在单个文件中。但是当你的程序变大，将它放在单个文件中就不可能了。</p>
<p>你可以将程序一些部分移动到一个单独的文件中，然后创建一个 <strong>头文件</strong>。</p>
<p>头文件看起来就像普通的 C 文件一样，但是它是以 <code>.h</code> 而不是 <code>.c</code> 结尾的。它里面的内容是 <strong>声明</strong>，而不是函数的实现和程序的其它部分。</p>
<p>你已经在第一次使用 <code>printf()</code> 函数或其它 I/O 函数的时候使用过头文件了，如果你要使用它，需要输入以下内容：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
</code></pre>
<p><code>#include</code> 是一个预处理器指令。</p>
<p>该预处理器会在标准库中寻找 <code>stdio.h</code> 文件，因为你使用了花括号包裹它。若要包含你自己的头文件，你需要使用引号（<code>"</code>），就像这样：</p>
<pre><code class="language-c">#include "myfile.h"
</code></pre>
<p>上述代码会让预处理器在当前文件夹内寻找 <code>myfile.h</code>。</p>
<p>你也可以使用文件夹结构的库：</p>
<pre><code class="language-c">#include "myfolder/myfile.h"
</code></pre>
<p>咱们看一个示例。这个程序计算自给定年份以来的年数：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int calculateAge(int year) {
  const int CURRENT_YEAR = 2020;
  return CURRENT_YEAR - year;
}

int main(void) {
  printf("%u", calculateAge(1983));
}
</code></pre>
<p>假设我想将 <code>caculateAge</code> 函数移到一个单独的文件中。</p>
<p>我创建一个名为 <code>calculate_age.c</code> 的文件：</p>
<pre><code class="language-c">int calculateAge(int year) {
  const int CURRENT_YEAR = 2020;
  return CURRENT_YEAR - year;
}
</code></pre>
<p>我还创建了一个名为 <code>calculate_age.h</code> 的文件，我在其中放入了 <em>函数原型</em>，除了函数体，它与 <code>.c</code> 文件中的函数完全相同：</p>
<pre><code class="language-c">int calculateAge(int year);
</code></pre>
<p>现在在主 <code>.c</code> 文件中，我们可以移除 <code>calculateAge()</code> 函数的定义，并且我们可以导入 <code>calculate_age.h</code>，它会让 <code>calculateAge()</code> 函数可用：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include "calculate_age.h"

int main(void) {
  printf("%u", calculateAge(1983));
}
</code></pre>
<p>别忘了编译多个文件组成的程序，你需要在命令行中列出它们，就像这样：</p>
<pre><code class="language-sh">gcc -o main main.c calculate_age.c
</code></pre>
<p>如果配置更加复杂，一个告诉编译器如何编译该程序的 Makefile 是必需的。</p>
<h2 id="the-preprocessor">预处理器</h2>
<p>预处理器是一个工具，当我们用 C 编程时，它对我们有很大的帮助。它是 C 标准的一部分，就像语言本身、编译器和标准库一样。</p>
<p>它解析我们的程序，确保编译器在处理之前获得所有需要的东西。</p>
<p>在实践中，它是做什么的呢？</p>
<p>例如，它查找你使用 <code>#include</code> 指令包含的所有头文件。</p>
<p>它还查看你使用 <code>#define</code> 定义的每个常量并将其替换为实际的值。</p>
<p>这只是一个开始。我提到了这两个操作，是因为它们是最常见的两个。预处理器能做的事情还有很多。</p>
<p>你有注意到 <code>#include</code> 和 <code>#define</code> 在开头有一个 <code>#</code> 吗？那在预处理器指令中是很常见的。如果某一行以 <code>#</code> 开始，它就会被预处理器关照。</p>
<h3 id="">条件</h3>
<p>我们能做的一件事情是使用条件让表达式决定程序的编译方式。</p>
<p>例如，我们可以检查 <code>DEBUG</code> 常量是否为 0：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

const int DEBUG = 0;

int main(void) {
#if DEBUG == 0
  printf("I am NOT debugging\n");
#else
  printf("I am debugging\n");
#endif
}
</code></pre>
<h3 id="">符号常量</h3>
<p>我们可以定义一个 <strong>符号常量（symbolic constant）</strong>：</p>
<pre><code class="language-c">#define VALUE 1
#define PI 3.14
#define NAME "Flavio"
</code></pre>
<p>当我们在自己的程序中使用 NAME 或 PI 或 VALUE 时，预处理器会在执行程序之前将名字替换成对应的值。</p>
<p>符号常量非常有用，因为我们可以给值名字，而不用在编译时创建变量。</p>
<h3 id="">宏</h3>
<p>我们还可以使用 <code>#define</code> 定义 <strong>宏（macro）</strong>。宏与符号常量之间的差别在于：宏可以接受一个参数，并且通常包含代码，而符号常量只是一个值：</p>
<pre><code class="language-c">#define POWER(x) ((x) * (x))
</code></pre>
<p>注意参数两侧的括号：当宏在预编译过程中被替换时，这是一个避免问题的好方法。</p>
<p>然后我们可以在代码中使用它，像这样：</p>
<pre><code class="language-c">printf("%u\n", POWER(4)); //16
</code></pre>
<p>它与函数之间的一个大差别就是：宏不会声明参数或返回值的类型，这在一些场景中可能很方便。</p>
<p>然而，宏的定义被限制成只有一行。</p>
<h3 id="ifdefined">If defined</h3>
<p>我们可以使用 <code>#ifdef</code> 来检查某个符号常量或宏是否被定义过：</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define VALUE 1

int main(void) {
#ifdef VALUE
  printf("Value is defined\n");
#else
  printf("Value is not defined\n");
#endif
}
</code></pre>
<p>我们也可以使用 <code>#ifndev</code> 检查对立面（宏未定义）。</p>
<p>我们还可以使用 <code>#if defined</code> 和 <code>#if !defined</code> 来达到同样的目的。</p>
<p>像这样将一些代码块包裹到单个块中是很常见的：</p>
<pre><code class="language-c">#if 0

#endif
</code></pre>
<p>这样可以临时防止程序运行，也可以使用一个 DEBUG 符号常量：</p>
<pre><code class="language-c">#define DEBUG 0

#if DEBUG
  // 当 DEBUG 不为 0 时，代码才会被发给编译器
#endif
</code></pre>
<h3 id="">你可以使用的预定义的符号常量</h3>
<p>预处理器还定义了很多你可以直接使用的符号常量，它们的名字的前后有两个下划线作为标识，包括：</p>
<ul>
<li><strong><code>__LINE__</code></strong> 代表源代码文件中的当前行</li>
<li><strong><code>__FILE__</code></strong> 代表文件的名字</li>
<li><strong><code>__DATE__</code></strong> 表示编译日期，格式为 <code>Mmm gg aaaa</code></li>
<li><strong><code>__TIME__</code></strong> 表示编译实践，格式为 <code>hh:mm:ss</code></li>
</ul>
<h2 id="conclusion">结语</h2>
<p>非常感谢阅读本手册！</p>
<p>我希望它将鼓励你去了解更多有关 C 的知识。</p>
<p>若想查看更多教程，你可以访问我的<a href="https://flaviocopes.com/">博客</a>。</p>
<p>通过 <a href="mailto:hey@flaviocopes.com">hey@flaviocopes.com</a> 给我发送反馈、勘误或者意见。</p>
<p>记住：<a href="https://flaviocopes.com/page/c-handbook/">你可以从这里获得这本手册的 PDF 或 ePub 版本</a>。</p>
<p>你可以在 Twitter <a href="https://twitter.com/flaviocopes">@flaviocopes</a> 联系我。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-c-beginners-handbook/">The C Beginner's Handbook: Learn C Programming Language basics in just a few hours</a>，作者：<a href="https://www.freecodecamp.org/news/author/flavio/">Flavio Copes</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python 终极指南：进阶之路 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你对数据科学、Web 开发、机器人或物联网感兴趣，那你一定要学 Python。由于 Python 被大量使用和广泛应用，它已经成为了增长最快的编程语言。 对于一个初学者或没有技术背景的人来说，学习 Python 是一个不错的选择。它的语法就像使用通俗英语说话和写作一样。以这个语法为例，它展示了和英语的相似性： print("Hello folks") 我们将在这篇教程中采用被广泛使用的 Python3，大多数 Python 的框架和库都支持这个版本。 > 注意：  任何高于 3.5.2 的版本都支持绝大多数库和框架。 索引：  1.  绪论  2.  安装  3.  Python shell  4.  注释  5.  打印  6.  缩进  7.  变量 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-ultimate-guide-to-python-from-beginner-to-intermediate-to-pro/</link>
                <guid isPermaLink="false">5ff988ac39641a0517d5376d</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 后端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Tue, 09 Feb 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/farzad-nazifi-p-xSl33Wxyc-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>如果你对数据科学、Web 开发、机器人或物联网感兴趣，那你一定要学 Python。由于 Python 被大量使用和广泛应用，它已经成为了增长最快的编程语言。</p>
<p>对于一个初学者或没有技术背景的人来说，学习 Python 是一个不错的选择。它的语法就像使用通俗英语说话和写作一样。以这个语法为例，它展示了和英语的相似性：</p>
<pre><code class="language-python">print("Hello folks")
</code></pre>
<p>我们将在这篇教程中采用被广泛使用的 <code>Python3</code>，大多数 Python 的框架和库都支持这个版本。</p>
<blockquote>
<p><strong>注意：</strong> 任何高于 3.5.2 的版本都支持绝大多数库和框架。</p>
</blockquote>
<h2 id="">索引：</h2>
<ol>
<li><a href="#%E7%BB%AA%E8%AE%BA">绪论</a></li>
<li><a href="#%E5%AE%89%E8%A3%85">安装</a></li>
<li><a href="#python-shell">Python shell</a></li>
<li><a href="#%E6%B3%A8%E9%87%8A">注释</a></li>
<li><a href="#%E6%89%93%E5%8D%B0">打印</a></li>
<li><a href="#%E7%BC%A9%E8%BF%9B">缩进</a></li>
<li><a href="#%E5%8F%98%E9%87%8F">变量</a></li>
<li><a href="#%E8%BF%90%E7%AE%97%E7%AC%A6">运算符</a></li>
<li><a href="#%E6%9D%A1%E4%BB%B6%E8%AF%AD%E5%8F%A5">条件语句</a></li>
<li><a href="#for-loops">For 循环</a></li>
<li><a href="#while-loops">While 循环</a></li>
<li><a href="#%E7%94%A8%E6%88%B7%E8%BE%93%E5%85%A5">用户输入</a></li>
<li><a href="#%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2">类型转换</a></li>
<li><a href="#%E5%AD%97%E5%85%B8">字典</a></li>
<li><a href="#%E5%88%97%E8%A1%A8">列表</a></li>
<li><a href="#%E5%85%83%E7%BB%84">元组</a></li>
<li><a href="#%E9%9B%86%E5%90%88">集合</a></li>
<li><a href="#%E5%87%BD%E6%95%B0%E4%B8%8E%E5%8F%82%E6%95%B0">函数与参数</a></li>
<li><a href="#Args">Args</a></li>
<li><a href="#%E5%85%B3%E9%94%AE%E5%AD%97%E5%8F%82%E6%95%B0">关键字参数</a></li>
<li><a href="#%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0">默认参数</a></li>
<li><a href="#kwargs">kwargs</a></li>
<li><a href="#%E4%BD%9C%E7%94%A8%E5%9F%9F">作用域</a></li>
<li><a href="#return-statement">Return 语句</a></li>
<li><a href="#lambda-expression">Lambda 表达式</a></li>
<li><a href="#%E5%88%97%E8%A1%A8%E6%8E%A8%E5%AF%BC%E5%BC%8F">列表推导式</a></li>
<li><a href="#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B">面向对象编程</a></li>
<li><a href="#%E7%B1%BB">类</a></li>
<li><a href="#%E6%96%B9%E6%B3%95">方法</a></li>
<li><a href="#%E5%AF%B9%E8%B1%A1">对象</a></li>
<li><a href="#%E6%9E%84%E9%80%A0%E5%99%A8">构造器</a></li>
<li><a href="#%E5%AE%9E%E4%BE%8B%E5%B1%9E%E6%80%A7">实例属性</a></li>
<li><a href="#%E7%B1%BB%E5%B1%9E%E6%80%A7">类属性</a></li>
<li><a href="#Self">Self</a></li>
<li><a href="#%E7%BB%A7%E6%89%BF">继承</a></li>
<li><a href="#Super">Super</a></li>
<li><a href="#%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF">多重继承</a></li>
<li><a href="#%E5%A4%9A%E6%80%81">多态</a></li>
<li><a href="#%E5%B0%81%E8%A3%85">封装</a></li>
<li><a href="#%E8%A3%85%E9%A5%B0%E5%99%A8">装饰器</a></li>
<li><a href="#%E5%BC%82%E5%B8%B8">异常</a></li>
<li><a href="#%E5%8C%85%E7%9A%84%E5%AF%BC%E5%85%A5">包的导入</a></li>
<li><a href="#json-handling">JSON 处理</a></li>
</ol>
<p><strong>注意：</strong> 这篇指南的开头部分是为初学者准备的。如果你拥有中级 Python 经验，随时可以使用上面的链接向前跳转。</p>
<h2 id="">绪论：</h2>
<p>根据 Github 2019 年度的 <a href="https://octoverse.github.com/#top-languages">octoverse</a> 报告，在开发者使用最多的语言中，Python 排名第二。</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/04/Screenshot-2020-04-29-at-6.53.10-PM.png" width="600" height="400" alt="Screenshot-2020-04-29-at-6.53.10-PM" loading="lazy">
    <figcaption style="text-align: center">Octoverse 编程语言演变图</figcaption>
</figure>
<p>在学习任何一门编程语言之前，了解该语言的由来是很有用的。Python 由荷兰程序员 <a href="https://en.wikipedia.org/wiki/Guido_van_Rossum">Guido van Rossum</a> 开发，于 1991 年发布。</p>
<p>Python 是一门解释型语言，它使用 <a href="https://en.wikipedia.org/wiki/CPython">CPython</a> 解释器将 Python 代码编译成字节码。对于初学者来说，你不需要对 CPython 有过多了解，但你必须知道 Python 内部是如何工作的。</p>
<p>Python 背后的哲学就是代码必须可读，这是通过缩进实现的。Python 还支持很多编程范式，比如函数式编程和面向对象编程。你将在阅读本文的过程中对它们有一个更好的理解。</p>
<p>大多数初学者脑中的基本问题就是一门编程语言能够做什么。这里是 Pyhton 的一些使用场景：</p>
<ul>
<li>服务端开发（Django，Flask）</li>
<li>数据科学（Pytorch，Tensor-flow）</li>
<li>数据分析/可视化（Matplotlib）</li>
<li>脚本（Beautiful Soup）</li>
<li>嵌入式开发</li>
</ul>
<blockquote>
<p><strong>注意：</strong> 我并不是特别为上面提到的任何库或框架背书，它们在各自的领域中都非常流行，也得到了广泛使用。</p>
</blockquote>
<h2 id="">安装：</h2>
<p>学习任何编程语言的第一步都是安装它。如今，大多数操作系统都自带 Python。你可以在终端执行以下命令，检查 Python 是否可用：</p>
<pre><code class="language-shell">python3 --version
</code></pre>
<p>输出如下：</p>
<pre><code class="language-shell">Python 3.7.0
</code></pre>
<p>注意：你的 Python 版本可能会有所不同。如果你已经安装过 Python 并且版本号在 3.5.2 以上，可以跳过这一部分。</p>
<p>对于电脑上没有 Python 的人来说，下面是安装步骤：</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/the-ultimate-guide-to-python-from-beginner-to-intermediate-to-pro/#windows-user-">Windows 用户</a></li>
<li><a href="https://www.freecodecamp.org/news/the-ultimate-guide-to-python-from-beginner-to-intermediate-to-pro/#mac-user-">Mac 用户</a></li>
<li><a href="https://www.freecodecamp.org/news/the-ultimate-guide-to-python-from-beginner-to-intermediate-to-pro/#linux-user-">Linux 用户</a></li>
</ul>
<h3 id="windows">Windows 用户：</h3>
<ul>
<li>打开 <a href="https://chinese.freecodecamp.org/news/the-ultimate-guide-to-python-from-beginner-to-intermediate-to-pro/50">Python 官网</a>。</li>
<li>点击下载按钮（下载 Python 3.8.2）[<strong>注意：</strong> 在你阅读本文时，版本可能会有所不同]。</li>
<li>前往下载目录，双击安装程序。</li>
<li>勾选“Add Python 3.x to PATH”，并单击“Install Now”。</li>
<li>安装完成后，你会收到一个“Setup was successful”的提示。再次使用上面的命令检查 python 是否配置正确。</li>
<li>使用命令 <code>python3 --version</code> 确认 Python 是否安装成功以及配置正确。</li>
</ul>
<h3 id="mac">Mac 用户</h3>
<ul>
<li>首先从应用商店安装 <a href="https://apps.apple.com/in/app/xcode/id497799835?mt=12">xcode</a>。</li>
<li>如果你想从终端安装 Xcode，可以使用以下命令：</li>
</ul>
<pre><code class="language-shell">xcode-select --install
</code></pre>
<ul>
<li>之后，我们将使用 brew 包管理器安装 Python。安装和配置 <a href="https://brew.sh/">brew</a> 的命令如下：</li>
</ul>
<pre><code class="language-shell">/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
</code></pre>
<ul>
<li>当 brew 设置完成，就使用下面这条命令更新所有过时的包：</li>
</ul>
<pre><code class="language-shell">brew update
</code></pre>
<ul>
<li>使用以下命令安装 Python：</li>
</ul>
<pre><code class="language-shell">brew install python3
</code></pre>
<ul>
<li>使用命令 <code>python3 --version</code> 确认 Python 是否安装成功以及配置正确。</li>
</ul>
<h3 id="linux">Linux 用户</h3>
<ul>
<li>使用 <code>apt</code> 安装 Python 的命令如下：</li>
</ul>
<pre><code class="language-shell">sudo apt install python3
</code></pre>
<ul>
<li>使用 <code>yum</code> 安装 Python 的命令如下：</li>
</ul>
<pre><code class="language-shell">sudo yum install python3
</code></pre>
<ul>
<li>使用命令 <code>python3 --version</code> 确认 Python 是否安装成功以及配置正确。</li>
</ul>
<h2 id="python-shell">Python shell：</h2>
<p>Shell 将会是你遇到的最有用的工具之一。 Python shell 允许我们在将任何想法集成到应用之前进行快速测试。</p>
<p>打开终端或者命令行提示符，输入 <code>python3</code> 命令，你会得到以下输出：</p>
<pre><code class="language-shell">➜ python3.7
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
&gt;&gt;&gt;
</code></pre>
<p>在本教程中，我们将利用你刚看到的 python3 shell 学习一些概念。从现在开始，只要我提到“打开 Python shell”，就表示你需要使用 <code>python3</code> 命令。</p>
<p>我们会创建一个以 <code>.py</code> 为扩展的文件 “testing”，用于学习剩下的概念。我们将使用以下命令运行这个文件：</p>
<pre><code class="language-shell">python3 testing.py
</code></pre>
<p>打开 Python shell，在 <code>&gt;&gt;&gt;</code> 标记后输入 <code>10 + 12</code>，你会得到 22：</p>
<pre><code class="language-python">&gt;&gt;&gt; 10 + 12
22
</code></pre>
<h2 id="">注释：</h2>
<p>注释（comment）帮助我们（和其他人）理解为什么要写某段代码，让代码的编写更加容易。注释的另一大作用就是帮助我们提高代码的可读性。</p>
<pre><code class="language-python"># Stay Safe
</code></pre>
<p>当你像上面这么写时，Python 解释器就会知道它是一个注释。<code>#</code> 之后的任何东西都不会被执行。</p>
<p>你可能想知道为什么应该使用注释。假设你是一名开发者，被指派了一个庞大的项目，这个项目有超过一千行代码。为了理解一切都是怎么工作的，你需要逐行阅读所有的代码。</p>
<p>有比这更好的解决办法吗？哈哈，有，就是注释。注释帮助我们理解为什么要写某段代码，它返回啥或者它干了啥。注释看作是这段代码的文档。</p>
<h2 id="">打印：</h2>
<p>除了编辑器的调试工具，最常帮助开发者解决问题的东西就是 print 语句了。print 语句是所有编程中最容易被低估的语法之一。</p>
<p>那么它是如何帮助我们调试问题的呢？假设你有一个模块，你想通过检查这个模块的执行过程理解或调试它。你有两个选择：要么使用调试工具，要么使用 print 语句。</p>
<p>并不是任何时候都可以使用调试工具。例如，如果你正在使用 Python shell，就没有调试工具可以用了。在这种情况下，print 语句可以帮我们。另一种情况就是你的应用正在运行，你可以添加一条显示日志的 print 语句，在运行时监视它们。</p>
<p>Python 提供了一个内置的 print 方法，语法如下：</p>
<pre><code class="language-python">print("Stay safe...")
</code></pre>
<h2 id="">缩进：</h2>
<p>Python 中另一个有趣的部分就是缩进（indentation）。为什么呢？答案很简单：缩进让代码易读、结构良好。Python 强制使用者遵守缩进规则，如果缩进不合适，你就会得到下面这个错误：</p>
<pre><code class="language-python">IndentationError: unexpected indent
</code></pre>
<p>看到了吧，即使是 Python 中的错误也这么易读和理解。你可能会在刚开始的时候因强制缩进而感到心烦，但是你会慢慢发现缩进是开发者的好朋友。</p>
<h2 id="">变量：</h2>
<p>顾名思义，变量（variable）就是能够变化的东西。在计算机程序中，变量则是引用内存位置的一种方式。</p>
<p>在大多数编程语言中，你需要指定变量的类型。但是在 Python 中，你不需要这么做。例如，要声明一个整型变量，C 语言中需要写 <code>int num = 5</code>，而 Python 中只需要写 <code>num = 5</code>。</p>
<p>打开 Python shell，然后一步一步执行：</p>
<ul>
<li><code>Integer</code>：可正可负，也可为零的数值，不含小数点。</li>
</ul>
<pre><code class="language-python">&gt;&gt;&gt; num = 5
&gt;&gt;&gt; print(num)
5
&gt;&gt;&gt; type(num)
&lt;class 'int'&gt;
</code></pre>
<p>如你所见，我们声明了一个 <code>num</code> 变量并赋值为 5。Python 内置的 <code>type</code> 方法可以被用来检查变量的类型。检查 <code>num</code> 的类型，我们得到的结果为 <code>&lt;class 'int'&gt;</code>。现在，只关注结果中的 <code>int</code>，它表示一个整数。</p>
<ul>
<li><code>Float</code>：和整数类似，但又有点细微的差别——浮点数是含有小数点的数值。</li>
</ul>
<pre><code class="language-python">&gt;&gt;&gt; num = 5.0
&gt;&gt;&gt; print(num)
5.0
&gt;&gt;&gt; type(num)
&lt;class 'float'&gt;
</code></pre>
<p>我们将带有一位小数的数值赋值给了 <code>num</code>。检查 <code>num</code> 的类型，我们得到的结果是 <code>float</code>。</p>
<ul>
<li><code>String</code>：由字符或整数构成，可以使用双引号或单引号表示。</li>
</ul>
<pre><code class="language-python">&gt;&gt;&gt; greet = "Hello user"
&gt;&gt;&gt; print(greet)
Hello user
&gt;&gt;&gt; type(greet)
&lt;class 'str'&gt;
</code></pre>
<p>我们在这里将一个字符串赋值给了 <code>greet</code>。从输出中你可以看到，它的类型为字符串。</p>
<ul>
<li><code>Boolean</code>：一个二元操作符，值为 True 或 False。</li>
</ul>
<pre><code class="language-python">&gt;&gt;&gt; is_available = True
&gt;&gt;&gt; print(is_available)
True
&gt;&gt;&gt; type(is_available)
&lt;class 'bool'&gt;
</code></pre>
<p>我们将 <code>is_available</code> 赋值为 True，它的类型为布尔。你只可以给布尔变量赋值为 <strong>True</strong> 或 <strong>False</strong>。记住，<strong>T</strong> 和 <strong>F</strong> 应该是大写，否则你会收到一个错误，如下：</p>
<pre><code class="language-shell">&gt;&gt;&gt; is_available = true
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
NameError: name 'true' is not defined
</code></pre>
<ul>
<li><code>NoneType</code>：在变量没有值的时候使用。</li>
</ul>
<pre><code class="language-python">&gt;&gt;&gt; num = None
&gt;&gt;&gt; print(num)
None
&gt;&gt;&gt; type(num)
&lt;class 'NoneType'&gt;
</code></pre>
<h2 id="">运算符：</h2>
<p>下图展示了 Python 中所有的运算符：</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/04/Screenshot-2020-04-30-at-12.28.55-PM.png" width="600" height="400" alt="Screenshot-2020-04-30-at-12.28.55-PM" loading="lazy">
    <figcaption>运算符表</figcaption>
</figure>
<p>我们一个一个地看。</p>
<h3 id="">算术运算符</h3>
<p>算术运算符包括加、减、乘、求幂、取模和向下取整除，一些运算符还有简写语法。</p>
<p>先声明两个变量，<code>a</code> 和 <code>b</code>。</p>
<pre><code class="language-python">&gt;&gt;&gt; a = 6 # 赋值
&gt;&gt;&gt; b = 2
</code></pre>
<p>尝试下基本算术运算符：</p>
<pre><code class="language-python">&gt;&gt;&gt; a + b # 加
8
&gt;&gt;&gt; a - b # 减
4
&gt;&gt;&gt; a * b # 乘
12
&gt;&gt;&gt; a / b # 除
3.0
&gt;&gt;&gt; a ** b # 求幂
36
</code></pre>
<p>为了测试其它的算术运算符，我们要改变一下 <code>a</code> 和 <code>b</code> 的值。</p>
<pre><code class="language-python">&gt;&gt;&gt; a = 7
&gt;&gt;&gt; b = 3
&gt;&gt;&gt; a % b # 取模
1
&gt;&gt;&gt; a // b # 向下取整除
2
</code></pre>
<p>Python 中也支持使用简写的算术运算符，你可以参照上面的图片进行测试，使用 <code>print</code> 语句打印出简写运算的结果。</p>
<h3 id="">比较运算符</h3>
<p>比较运算符包括等于、大于和小于。</p>
<pre><code class="language-python">&gt;&gt;&gt; a = 5 # 赋值
&gt;&gt;&gt; b = 2 # 赋值
&gt;&gt;&gt; a &gt; b # 大于
True
&gt;&gt;&gt; a &lt; b # 小于
False
&gt;&gt;&gt; a == b # 等于
False
&gt;&gt;&gt; a &gt;= 5 # 大于等于
True
&gt;&gt;&gt; b &lt;= 1 # 小于等于
False
</code></pre>
<h3 id="">逻辑运算符</h3>
<p>逻辑运算符包括非（not）、与（and）、或（or）。</p>
<pre><code class="language-python">&gt;&gt;&gt; a = 10
&gt;&gt;&gt; b = 2
&gt;&gt;&gt; a == 2 and b == 10 # 与
False
&gt;&gt;&gt; a == 10 or b == 10 # 或
True
&gt;&gt;&gt; not(a == 10) # 非
False
&gt;&gt;&gt; not(a == 2)
True
</code></pre>
<h2 id="">条件语句：</h2>
<p>顾名思义，条件语句用于计算条件的真假。</p>
<p>很多时候，你需要在开发过程中根据特定的条件做不同的事情。在这种情况下，条件语句就非常有用了。Python 中的条件语句包括 if、elif 和 else。</p>
<p>我们可以比较变量，检查变量是否为一些值。如果变量为布尔类型的话，就检查它是真还是假。打开 Python shell，逐步执行：</p>
<p><strong>条件 1：</strong> 我们有一个整数和三个条件。第一个是 <code>if</code> 条件，它检查数字是否等于 10。</p>
<p>第二个是 <code>elif</code> 条件，它检查数字是否小于 10。</p>
<p>最后一个条件是 <code>else</code>，它在以上两个条件匹配失败时执行。</p>
<pre><code class="language-python">&gt;&gt;&gt; number = 5
&gt;&gt;&gt; if number == 10:
...     print("Number is 10")
... elif number &lt; 10:
...     print("Number is less than 10")
... else:
...     print("Number is more than 10")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Number is less than 10
</code></pre>
<p><strong>注意：</strong> 并不是只能在 <code>if</code> 条件中检查两个条件是否相等，你也可以使用 <code>elif</code>。</p>
<p><strong>Condition Number 2:</strong> We have a boolean and 2 conditions here. Have you noticed how we are checking if the condition is true? If  <code>is_available</code>, then print "Yes it is available", else print "Not available".</p>
<p><strong>条件 2：</strong> 我们有一个布尔值和两个条件。你有注意到我们是如何检查条件为真吗？如果 <code>is_available</code>，就打印“Yes it is available”，否则打印“Not available”。</p>
<pre><code class="language-python">&gt;&gt;&gt; is_available = True
&gt;&gt;&gt; if is_available:
...     print("Yes it is available")
... else:
...     print("Not available")
...

</code></pre>
<p>Output:</p>
<p>输出：</p>
<pre><code class="language-shell">Yes it is available
</code></pre>
<p><strong>条件 3：</strong> 使用取反运算符反转条件 2。</p>
<pre><code class="language-python">&gt;&gt;&gt; is_available = True
&gt;&gt;&gt; if not is_available:
...     print("Not available")
... else:
...     print("Yes it is available")
...

</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Yes it is available
</code></pre>
<p><strong>条件 4：</strong> 将 data 声明为 None，然后检查 data 是否可用。</p>
<pre><code class="language-python">&gt;&gt;&gt; data = None
&gt;&gt;&gt; if data:
...     print("data is not none")
... else:
...     print("data is none")
...

</code></pre>
<p>输出：</p>
<pre><code class="language-shell">data is none
</code></pre>
<p><strong>条件 5：</strong> 使用行内 if，语法如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; num_a = 10
&gt;&gt;&gt; num_b = 5
&gt;&gt;&gt; if num_a &gt; num_b: print("num_a is greater than num_b")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">num_a is greater than num_b
</code></pre>
<p><strong>条件 6：</strong> 使用行内 if else，语法如下：</p>
<pre><code class="language-python">expression_if_true if condition else expression_if_false

</code></pre>
<p>示例：</p>
<pre><code class="language-python">&gt;&gt;&gt; num = 5
&gt;&gt;&gt; print("Number is five") if num == 5 else print("Number is not five")
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Number is five
</code></pre>
<p><strong>条件 7：</strong> 使用嵌套的 if-else 语句，语法如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; num = 25
&gt;&gt;&gt; if num &gt; 10:
...     print("Number is greater than 10")
...     if num &gt; 20:
...             print("Number is greater than 20")
...     if num &gt; 30:
...             print("Number is greater than 30")
... else:
...     print("Number is smaller than 10")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Number is greater than 10
Number is greater than 20
</code></pre>
<p><strong>条件 8：</strong> 在条件语句中使用 <code>and</code> 运算符，它只有在两个条件同时满足时才会执行。</p>
<pre><code class="language-python">&gt;&gt;&gt; num = 10
&gt;&gt;&gt; if num &gt; 5 and num &lt; 15:
...     print(num)
... else:
...     print("Number may be small than 5 or larger than 15")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">10
</code></pre>
<p>由于我们的数字在 5 到 15 之间，所以我们得到的结果是 10。</p>
<p><strong>条件 9：</strong> 使用 <code>or</code> 运算符，它在任一条件为真时执行。</p>
<pre><code class="language-python">&gt;&gt;&gt; num = 10
&gt;&gt;&gt; if num &gt; 5 or num &lt; 7:
...     print(num)
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">10
</code></pre>
<p>因为 <code>num</code> 的值为 10，并且我们的第二个条件要求 <code>num</code> 小于 7，你是不是很困惑？为什么输出是 10 呢？因为 <code>or</code> 只要匹配上任意一个条件，就会执行。</p>
<h2 id="for-loops">For 循环：</h2>
<p>另一个在所有编程语言中都非常有用的方法就是迭代器（iterator）。如果你不得不多次实现某个东西，你会怎么做？</p>
<pre><code class="language-python">print("Hello")
print("Hello")
print("Hello")
</code></pre>
<p>这是一种方式。但是，如果必须做一百或一千次，你就要非写大量的 print 语句不可。有一种更好的处理方式——使用迭代器或循环。我们可以使用 <code>for</code> 循环，也可以使用 <code>while</code> 循环。</p>
<p>这里使用了 range 方法，它给出了一个区间，循环应该在这个区间内重复执行。默认情况下，range 的开始点为 0。</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in range(3):
...     print("Hello")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Hello
Hello
Hello
</code></pre>
<p>你也可以使用 <code>range(1,3)</code> 这种方式声明区间。</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in range(1,3):
...     print("Hello")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Hello
Hello
</code></pre>
<p>因为我们声明了区间，所以“Hello”只打印了两次。你可以把区间看成 <code>Number on right - Number on left</code>。</p>
<p>你还可以将 else 语句添加到 for 循环。</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in range(3):
...     print("Hello")
... else:
...     print("Finished")
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Hello
Hello
Hello
Finished
</code></pre>
<p>循环先迭代了 3 次（3 - 0），else 语句在迭代完成后立即执行。</p>
<p>我们也可以将一个 for 循环嵌入到另一个 for 循环之中。</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in range(3):
...     for j in range(2):
...             print("Inner loop")
...     print("Outer loop")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Inner loop
Inner loop
Outer loop
Inner loop
Inner loop
Outer loop
Inner loop
Inner loop
Outer loop
</code></pre>
<p>如你所见，内层循环的打印语句执行了两次，之后外层循环的打印语句执行了一次，然后又是两次内层循环。所以这里在发生什么呢？如果你感到困惑，这么想一想：</p>
<ul>
<li>解释器一上来就看见了一个 <code>for</code> 循环，它再次向下，发现还有另一个 <code>for</code> 循环。</li>
<li>现在它会执行两次内层的 <code>for</code> 循环，然后退出。内层循环执行完之后，编译器就会得知外层循环要求它再重复执行两次。</li>
<li>解释器再次执行，遇到内层循环，然后重复这个过程。</li>
</ul>
<p>你还可以选择通过某个 <code>for</code> 循环条件，通过在这里是什么意思呢？不论 for 循环在何时发生，只要解释器看到了 <code>pass</code> 语句，它什么都不会做，直接跳到下一行。</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in range(3):
...     pass
...
</code></pre>
<p>你不会在 shell 中得到任何输出。</p>
<h2 id="while-loops">While 循环：</h2>
<p>Python 中还有一种循环或迭代器，它就是 <code>while</code> 循环。我们可以使用 <code>while</code> 循环得到与 <code>for</code> 循环一样的结果。</p>
<pre><code class="language-python">&gt;&gt;&gt; i = 0
&gt;&gt;&gt; while i &lt; 5:
...     print("Number", i)
...     i += 1
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Number 0
Number 1
Number 2
Number 3
Number 4
</code></pre>
<p>不论你何时使用 while 循环，都要记得添加一个递增语句，或者一个在某种情况下能够结束 while 循环的语句。否则，循环会一直执行下去。</p>
<p>另一种方式就是在 <code>while</code> 循环中加入一个 <code>break</code> 语句，它会打破循环。</p>
<pre><code class="language-python">&gt;&gt;&gt; i = 0
&gt;&gt;&gt; while i &lt; 5:
...     if i == 4:
...             break
...     print("Number", i)
...     i += 1
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Number 0
Number 1
Number 2
Number 3
</code></pre>
<p>在这里，如果发现 <code>i</code> 的值为 4，我们就打破 <code>while</code> 循环。</p>
<p>另一种方式是在 <code>while</code> 循环中加入一个 <code>else</code> 语句，它会在 while 循环完成后执行。</p>
<pre><code class="language-python">&gt;&gt;&gt; i = 0
&gt;&gt;&gt; while i &lt; 5:
...     print("Number", i)
...     i += 1
... else:
...     print("Number is greater than 4")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Number 0
Number 1
Number 2
Number 3
Number 4
Number is greater than 4
</code></pre>
<p><code>continue</code> 语句可以用来跳出当前循环，直接进到下次循环。</p>
<pre><code class="language-python">&gt;&gt;&gt; i = 0
&gt;&gt;&gt; while i &lt; 6:
...     i += 1
...     if i == 2:
...             continue
...     print("number", i)
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">number 1
number 3
number 4
number 5
number 6
</code></pre>
<h2 id="">用户输入：</h2>
<p>假设你正在构建一个命令行应用，现在你需要根据用户的输入执行不同的操作。为了达到这个目的，你可以使用 Python 内置的 <code>input</code> 方法。</p>
<p>实现的语法很简单，如下所示：</p>
<pre><code class="language-python">variable = input(".....")
</code></pre>
<p>示例：</p>
<pre><code class="language-python">&gt;&gt;&gt; name = input("Enter your name: ")
Enter your name: Sharvin
</code></pre>
<p>当你使用 <code>input</code> 方法时，你会在按下回车键之后收到一个提示，提示的内容就是你输入到 <code>input</code> 方法的文本。我们来检查一下赋值有没有成功：</p>
<pre><code class="language-python">&gt;&gt;&gt; print(name)
Sharvin
</code></pre>
<p>就是这个！它运行得很好，<code>Sharvin</code> 就是输入的字符串。</p>
<pre><code class="language-python">&gt;&gt;&gt; type(name)
&lt;class 'str'&gt;
</code></pre>
<p>我们用另一个例子试一下。这一次我们会把它赋值给一个整数，而不是字符串，然后会检查它的类型。</p>
<pre><code class="language-python">&gt;&gt;&gt; date = input("Today's date: ")
Today's date: 12
&gt;&gt;&gt; type(date)
&lt;class 'str'&gt;
</code></pre>
<p>又困惑了吗？我们输入了一个整数 12，但它还是告诉我们类型为字符串。这并不是一个 bug，它正是输入的工作机制。要将字符串转成整数，我们需要使用类型转换。</p>
<h2 id="">类型转换：</h2>
<p>我们看到 <code>input</code> 方法为整数返回了字符串。如果我们现在想把这个输出和另一个整数进行比较，就需要一种将其转换回整数的方式。</p>
<pre><code class="language-python">&gt;&gt;&gt; date_to_int = int(date)
&gt;&gt;&gt; type(date_to_int)
&lt;class 'int'&gt;
</code></pre>
<p>这里我们使用了用户输入一节声明的 date，使用 Python 内置的 <code>int</code> 方法将它转换成一个整数。这就是类型转换（typecasting）。</p>
<p>你基本上可以进行下列转换：</p>
<ul>
<li>整数到字符串：<code>str()</code></li>
<li>字符串到整数：<code>int()</code></li>
<li>整数到浮点数：<code>float()</code></li>
</ul>
<blockquote>
<p>注意：浮点数到整数的转换也是可能的。</p>
</blockquote>
<pre><code class="language-python">&gt;&gt;&gt; type(date)
&lt;class 'str'&gt;

# 从字符串转换到浮点数
&gt;&gt;&gt; date_to_float = float(date)
&gt;&gt;&gt; type(date_to_float)
&lt;class 'float'&gt;
# 从浮点数转换到字符串
&gt;&gt;&gt; date_to_string = str(date_to_float)
&gt;&gt;&gt; type(date_to_string)
&lt;class 'str'&gt;

</code></pre>
<h2 id="">字典</h2>
<p>假如你想保存一些用户的详细资料，你会怎么保存它们呢？没错，我们可以用变量保存它们，像下面这样：</p>
<pre><code class="language-python">&gt;&gt;&gt; fname = "Sharvin"
&gt;&gt;&gt; lname = "Shah"
&gt;&gt;&gt; profession = "Developer"
</code></pre>
<p>要访问变量的值，可以这么做：</p>
<pre><code class="language-python">&gt;&gt;&gt; print(fname)
Sharvin
</code></pre>
<p>但是这是访问用户资料最优雅的方式吗？并不是。让我们把数据存到键值对形式的字典（dictionary）中吧，它会让用户资料的访问变得更加友好。</p>
<p>那么什么是字典呢？字典是一种无序、可变（即它可以被更新）的集合。</p>
<p>字典的格式如下：</p>
<pre><code class="language-json">data = {
    "key" : "value"
}
</code></pre>
<p>让我们通过一个示例进一步理解字典吧：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details = {
...     "fname": "Sharvin",
...     "lname": "Shah",
...     "profession": "Developer"
... }
</code></pre>
<h3 id="">如果访问字典中的值</h3>
<p>有两种访问字典中的值的方式，我们将逐一查看并调试它们，从而找出哪种方式更好。</p>
<p>方法一：使用以下语法访问 <code>user_details</code> 字典中键为 <code>fname</code> 的值：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details["fname"]
'Sharvin'
</code></pre>
<p>方法二：使用 <code>get</code> 访问 <code>user_details</code> 字典中键为 <code>fname</code> 的值：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details.get("fname")
'Sharvin'
</code></pre>
<p>我知道方法一看起来更容易理解。当我们尝试访问一个字典中不存在的数据的时，它的问题体现出来了。</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details["age"]
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
KeyError: 'age'
</code></pre>
<p>我们得到了一个 KeyError，表示那个键不可用。我们使用方法二试一下这种情况。</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details.get("age")
</code></pre>
<p>控制台空空如也。让我们进一步调试，搞清楚为何会这样。将 <code>get</code> 操作赋值给一个 age 变量，然后将其打印在控制台。</p>
<pre><code class="language-python">&gt;&gt;&gt; age = user_details.get("age")
&gt;&gt;&gt; print(age)
None
</code></pre>
<p>所以，<code>get</code> 方法会在找不到键时把值设置为 None。正因为如此，我们才不会得到任何错误。现在你或许知道该使用哪个了吧。大多数情况下，方法二更合适，但是对于需要严格检查的条件，还是应该使用方法一。</p>
<h3 id="">如何检查一个键是否存在</h3>
<p>你或许想知道如何检查字典中是否有某个键。Python 为此提供了一个内置的方法 <code>keys()</code>。</p>
<pre><code class="language-python">&gt;&gt;&gt; if "age" in user_details.keys():
...     print("Yes it is present")
... else:
...     print("Not present")
...
</code></pre>
<p>输出如下：</p>
<pre><code class="language-shell">Not present
</code></pre>
<p>如果我们想检查字典是否为空呢？为了便于理解，先声明一个空的字典：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details = {}
</code></pre>
<p>我们直接在字典上使用 if-else，它要么在数据存在时返回 True，要么就在字典为空时返回 False。</p>
<pre><code class="language-python">&gt;&gt;&gt; if user_details:
...     print("Not empty")
... else:
...     print("Empty")
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Empty
</code></pre>
<p>我们也可以使用 Python 内置的 <code>bool</code> 方法检查字典是否为空。记住：如果字典为空，bool 会返回 False，否则会返回 True。</p>
<pre><code class="language-python">&gt;&gt;&gt; bool(user_details)
False

</code></pre>
<h3 id="">如果更新已有键的值</h3>
<p>现在我们知道了如何获取某个特定的键以及如何检查键是否存在，但是你怎么在字典中更新某个键呢？</p>
<p>声明一个字典，如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details = {
...     "fname":"Sharvin",
...     "lname": "Shah",
...     "profession": "Developer"
... }
</code></pre>
<p>使用以下语法更新对应的值：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details["profession"] = "Software Developer"
&gt;&gt;&gt; print(user_details)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer'}
</code></pre>
<p>在字典中，更新键的值的方式与给变量赋值的方式一摸一样。</p>
<h3 id="">如何添加键-值对</h3>
<p>下一个问题是如何添加一个新的值到字典中？让我们添加一个 <code>age</code> 键吧，值为 100。</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details["age"] = "100"
&gt;&gt;&gt; print(user_details)
{'fname': 'Sharvin', 'lname': 'Shah', 'profession': 'Software Developer', 'age': '100'}
</code></pre>
<p>如你所见，一个新的键-值对已经被添加到字典中。</p>
<h3 id="">如何移除键-值对</h3>
<p>要将一个键-值对从字典中移除，你可以使用 Python 内置的 <code>pop</code>方法。</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details.pop("age")
'100'

</code></pre>
<p>这将 <code>age</code> 键-值对从 <code>user_details</code> 字典中移除，我们也可以使用 <code>del</code> 运算符删除这个值。</p>
<pre><code class="language-python">&gt;&gt;&gt; del user_details["age"]

</code></pre>
<p><code>del</code> 方法也可以用来 <strong>删除整个字典</strong>，语法为 <code>del user_details</code>。</p>
<h3 id="">如何复制字典</h3>
<p>字典是不能使用传统方式复制的。例如，你不能像下面这样将 <code>dictA</code> 的值复制到 <code>dictB</code>：</p>
<pre><code class="language-python">dictA = dictB
</code></pre>
<p>你需要使用 <code>copy</code> 方法完成值的复制。</p>
<pre><code class="language-python">&gt;&gt;&gt; dictB = user_details.copy()

</code></pre>
<h2 id="">列表：</h2>
<p>假如你有一堆没有标签的数据，换句话说，每条数据都没有定义它的键。你会怎么保存它呢？这个时候就该列表出场了，数据的定义如下：</p>
<pre><code class="language-python">data = [ 1, 5, "xyz", True ]
</code></pre>
<p>列表（list）是一种随机、有序和可变的集合。</p>
<h3 id="">如何访问列表元素</h3>
<p>让我们试着访问列表中的第一个元素：</p>
<pre><code class="language-python">&gt;&gt;&gt; data[1]
5
</code></pre>
<p>等一下，这里发生了什么？我们尝试访问第一个元素，但是却得到了第二个元素。为什么？</p>
<p>列表的索引从零开始。我这么说的意思是什么呢？元素位置的索引从零开始。访问列表元素的语法如下：</p>
<pre><code class="language-python">list[position_in_list]
</code></pre>
<p>要访问第一个元素，我们需要这么做：</p>
<pre><code class="language-python">&gt;&gt;&gt; data[0]
1
</code></pre>
<pre><code class="language-python">&gt;&gt;&gt; data[2:4]
['xyz', True]
</code></pre>
<p>第一个值表示开始位置，而最后一个值表示我们想要访问值的前一个位置。</p>
<h3 id="">如何向列表添加数据项</h3>
<p>要将一个数据项添加到列表中，我们需要使用 Python 提供的 append 方法。</p>
<pre><code class="language-python">&gt;&gt;&gt; data.append("Hello")

</code></pre>
<h3 id="">如何改变数据项的值</h3>
<p>改变数据项的值的语法如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; data[2] = "abc"

</code></pre>
<p>我们可以使用 Python 内置的 <code>remove</code> 方法从列表中删除一个数据项。</p>
<pre><code class="language-python">&gt;&gt;&gt; data.remove("Hello")
&gt;&gt;&gt; data
[1, 5, 'abc', True]
</code></pre>
<h3 id="">如何遍历列表</h3>
<p>我们也可以遍历列表，从中找出某个元素，然后操作这个元素。</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in data:
...     print(i)
...
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">1
5
abc
True

</code></pre>
<h3 id="">如何检查一个数据项存在与否</h3>
<p>要检查某个数据项是否存在于列表中，我们可以像下面这样使用 if：</p>
<pre><code class="language-python">&gt;&gt;&gt; if 'abc' in data:
...     print("yess..")
...
yess..
</code></pre>
<h3 id="">如何复制列表数据</h3>
<p>要将一个列表的数据复制到另一个列表，我们需要使用 <code>copy</code> 方法。</p>
<pre><code class="language-python">&gt;&gt;&gt; List2 = data.copy()
&gt;&gt;&gt; List2
[1, 5, 'abc', True]
</code></pre>
<h3 id="">如何检查列表长度</h3>
<p>我们可以使用 Python 内置的 <code>len</code> 方法检查列表的长度。</p>
<pre><code class="language-python">&gt;&gt;&gt; len(data)
4
</code></pre>
<h3 id="">如何连接两个列表</h3>
<p>我们可以使用 <code>+</code> 操作符连接两个列表。</p>
<pre><code class="language-python">&gt;&gt;&gt; list1 = [1, 4, 6, "hello"]
&gt;&gt;&gt; list2 = [2, 8, "bye"]
&gt;&gt;&gt; list1 + list2
[1, 4, 6, 'hello', 2, 8, 'bye']
</code></pre>
<p>如果我们尝试访问一个在列表中不可用的元素位置会怎样？我们会得到一个 <code>list index out of range error</code>。</p>
<pre><code class="language-python">&gt;&gt;&gt; list1[6]
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
IndexError: list index out of range
</code></pre>
<h2 id="">元组：</h2>
<p>元组（tuple）是一种有序但不可变（即数据不能被改变）的数据类型。</p>
<p>创建一个元组：</p>
<pre><code class="language-python">&gt;&gt;&gt; data = ( 1, 3 , 5, "bye")
&gt;&gt;&gt; data
(1, 3, 5, 'bye')
</code></pre>
<h3 id="">如何访问元组中的元素</h3>
<p>我们可以用访问列表元素的方式访问元组中的元素：</p>
<pre><code class="language-python">&gt;&gt;&gt; data[3]
'bye'
</code></pre>
<p>用以下方式访问索引范围：</p>
<pre><code class="language-python">&gt;&gt;&gt; data[2:4]
(5, 'bye')
</code></pre>
<h3 id="">如何改变元组的值</h3>
<p>如果你正在思考如何改变元组的值，我就真拿你当朋友了。我们不能改变元组的值，因为它是不可变的。如果我们尝试改变元组的值，会得到如下错误：</p>
<pre><code class="language-python">&gt;&gt;&gt; data[1] = 8
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
TypeError: 'tuple' object does not support item assignment
</code></pre>
<p>有一种改变元组的值的变通方法：</p>
<pre><code class="language-python">&gt;&gt;&gt; data = ( 1, 3 , 5, "bye")
&gt;&gt;&gt; data_two = list(data) # 转换成列表
&gt;&gt;&gt; data_two[1] = 8 # 列表是可变的，更新值
&gt;&gt;&gt; data = tuple(data_two) # 再次转换成元组
&gt;&gt;&gt; data
(1, 8, 5, 'bye')

</code></pre>
<p>我们见过的其它列表方法都适用于元组。</p>
<p><strong>[注意：一旦元组被创建出来，就不能添加新值到其中了]</strong>。</p>
<h2 id="">集合</h2>
<p>集合（set）是 Python 中的另一种数据类型，它是无序的，没有索引。集合的声明方式如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; data = { "hello", "bye", 10, 15 }
&gt;&gt;&gt; data
{10, 15, 'hello', 'bye'}
</code></pre>
<h3 id="">如何访问值</h3>
<p>由于集合没有索引，所以我们不能直接访问集合中的值。因此，我们需要使用 for 循环：</p>
<pre><code class="language-python">&gt;&gt;&gt; for i in data:
...     print(i)
...

</code></pre>
<h3 id="">如何改变一个值</h3>
<p>一旦集合被创建，值就不能被改变。</p>
<h3 id="">如何添加一个数据项</h3>
<p>Python 提供的 <code>add</code> 方法就可以将数据项添加到集合。</p>
<pre><code class="language-python">&gt;&gt;&gt; data.add("test")
&gt;&gt;&gt; data
{10, 'bye', 'hello', 15, 'test'}
</code></pre>
<h3 id="">如何检查长度</h3>
<p>我们可以使用 <code>len</code> 方法检查集合的长度。</p>
<pre><code class="language-python">&gt;&gt;&gt; len(data)
5
</code></pre>
<h3 id="">如何删除一个数据项</h3>
<p>使用 <code>remove</code> 方法移除数据项：</p>
<pre><code class="language-python">&gt;&gt;&gt; data.remove("test")
&gt;&gt;&gt; data
{10, 'bye', 'hello', 15}
</code></pre>
<h2 id="">函数与参数</h2>
<p>函数（Function）是一种声明我们想要执行的操作的简单方式。在函数的帮助下，你可以根据操作拆分逻辑。</p>
<p>函数就是一个代码块，它帮助我们复用重复的逻辑。函数既可以是内置的也可以是用户自定义的。</p>
<p>我们使用 <code>def</code> 关键字声明函数，下面是函数的语法：</p>
<pre><code class="language-python">&gt;&gt;&gt; def hello_world():
...     print("Hello world")
...

</code></pre>
<p>我们声明了一个函数，它会在被调用时打印出“Hello World”语句。调用函数的语法如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; hello_world()
</code></pre>
<p>我们会得到以下结果：</p>
<pre><code class="language-shell">Hello world
</code></pre>
<p>记住，函数调用中的 <code>()</code> 括号表示执行函数本身。你可以把圆括号去掉试一下。</p>
<pre><code class="language-python">&gt;&gt;&gt; hello_world
</code></pre>
<p>你会得到以下输出：</p>
<pre><code class="language-shell">&lt;function hello_world at 0x1083eb510&gt;
</code></pre>
<p>当我们把圆括号从函数调用上去掉时，它会给我们一个函数引用。从上面可以看出：<code>function hello_world</code> 的引用指向了 <code>0x1083eb510</code> 这个内存地址。</p>
<p>如果你要执行一个加法操作，你可以先声明 <code>a</code> 和 <code>b</code>，然后把它们相加。</p>
<pre><code class="language-python">&gt;&gt;&gt; a = 5
&gt;&gt;&gt; b = 10
&gt;&gt;&gt; a + b
15
</code></pre>
<p>这是一种方式。但是，如果 <code>a</code> 和 <code>b</code> 的值变了，你就需要再这么来一次。</p>
<pre><code class="language-python">&gt;&gt;&gt; a = 5
&gt;&gt;&gt; b = 10
&gt;&gt;&gt; a + b
15
&gt;&gt;&gt; a = 2
&gt;&gt;&gt; b = 11
&gt;&gt;&gt; a + b
13
</code></pre>
<p>这看起来仍然是可行的。现在，假设我们需要进行一百次两数相加操作，每次相加的两个数都不同，这就有得做了。别担心，我们有函数，它可以解决这个问题。</p>
<pre><code class="language-python">&gt;&gt;&gt; def add(a,b):
...     print(a+b)
...

</code></pre>
<p>我们在这里把 <code>a</code> 和 <code>b</code> 作为了 <code>add</code> 函数的必备参数（compulsory argument），调用这个函数的语法如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; add(10,5)
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">15
</code></pre>
<p>定义一个函数并使用它是不是很容易？如果我们不传递参数会怎么样？</p>
<pre><code class="language-python">&gt;&gt;&gt; add()
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
TypeError: add() missing 2 required positional arguments: 'a' and 'b'
</code></pre>
<p>Python 抛出了一个 TypeError，告知我们这个函数需要有两个参数。</p>
<p>如果我们传递了第三个参数，你能猜出来会发生什么吗？</p>
<pre><code class="language-python">&gt;&gt;&gt; add(10,5,1)
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
TypeError: add() takes 2 positional arguments but 3 were given
</code></pre>
<p>Python 会告诉我们：我们传递了三个参数，但是只有两个位置参数。</p>
<p>所以当我们不知道函数的参数数量时该怎么做呢？我们可以使用 args 和 kwargs 解决这个问题。</p>
<h2 id="args">Args：</h2>
<p>当你不知道要给函数传递多少个参数时，可以使用 args 和 kwargs（kwargs 会在下面进行讨论）。</p>
<p>要给函数传递 n 个参数，我们使用 args。在参数的前面添加一个 <code>*</code>。</p>
<blockquote>
<p>记住：当你在前面添加 <code>*</code> 时，你会得到一个由参数构成的元组。</p>
</blockquote>
<pre><code class="language-python">&gt;&gt;&gt; def add(*num):
...     print(num)
...
</code></pre>
<p>这里的 <code>*num</code> 是 args 的一个实例。当我们调用 <code>add</code> 函数时，可以传入 n 个参数，也不会有 <code>TypeError</code> 被抛出。</p>
<pre><code class="language-python">&gt;&gt;&gt; add(1,2,3)
(1, 2, 3)

</code></pre>
<p>现在我们使用 Python 内置的 <code>sum</code> 函数进行加法操作：</p>
<pre><code class="language-python">&gt;&gt;&gt; def add(*num):
...     print(sum(num))
...

</code></pre>
<p>当我们调用 add 函数时，就会得到以下输出：</p>
<pre><code class="language-python">&gt;&gt;&gt; add(1,2,3) # 函数调用
6
&gt;&gt;&gt; add(1,2,3,4) # 函数调用
10
</code></pre>
<h2 id="">关键字参数：</h2>
<p>有时候，我们并不知道传递给函数的参数顺序。这时就可以使用关键词参数，不管我们以何种顺序传递参数，函数都会知道对应的参数值。来看这个示例：</p>
<pre><code class="language-python">&gt;&gt;&gt; def user_details(username, age):
...     print("Username is", username)
...     print("Age is", age)
...
</code></pre>
<p>像下面这样调用这个函数：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details("Sharvin", 100)
</code></pre>
<p>输出如下：</p>
<pre><code class="language-shell">Username is Sharvin
Age is 100
</code></pre>
<p>这看起来没错，但是如果我们用这种方式调用这个函数呢：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details(100, "Sharvin")
</code></pre>
<p>输出如下：</p>
<pre><code class="language-shell">Username is 100
Age is Sharvin
</code></pre>
<p>这看起来不太对。<code>username</code> 取了 100 这个值，而 <code>age</code> 取了“Sharvin”这个值。在我们不知道参数的顺序时，可以在调用函数时使用关键词参数：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details(age=100, username="Sharvin")

</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Username is Sharvin
Age is 100
</code></pre>
<h2 id="">默认参数：</h2>
<p>有时，我们并不确定某个参数是否会在函数被调用时得到值。这种情况下可以使用默认参数，例如：</p>
<pre><code class="language-python">&gt;&gt;&gt; def user_details(username, age = None):
...     print("Username is", username)
...     print("Age is", age)
...
</code></pre>
<p>这里将 <code>None</code> 赋给了 age 参数，如果我们在调用函数时不传递第二个参数，它就会自动将 None 作为默认值。</p>
<p>让我们调用一下这个函数吧：</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details("Sharvin")
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Username is Sharvin
Age is None
</code></pre>
<p>如果我们传了第二个参数，它就会覆盖 None 并使用自己作为参数值。</p>
<pre><code class="language-python">&gt;&gt;&gt; user_details("Sharvin", 200)
Username is Sharvin
Age is 200
</code></pre>
<p>但是，如果我们将第一个参数设置为默认并把第二个参数设置为必备参数，会怎么样呢？打开 Python shell，一探究竟：</p>
<pre><code class="language-python">&gt;&gt;&gt; def user_details(username=None, age):
...     print("Username is", username)
...     print("Age is", age)
...
</code></pre>
<p>你会得到如下错误：</p>
<pre><code class="language-shell">  File "&lt;stdin&gt;", line 1
SyntaxError: non-default argument follows default argument
</code></pre>
<blockquote>
<p><strong>记住：</strong> 所有的必备参数必须先于默认参数声明。</p>
</blockquote>
<h2 id="kwargs">Kwargs：</h2>
<p>有时你并不知道会有多少个关键字参数会被传递给函数，这种情况可以使用 Kwargs。</p>
<p>要使用 kwargs，我们要把它放在 <strong>参数之前</strong>。</p>
<blockquote>
<p><strong>记住：</strong> 当你在前面附加一个 <code>**</code> 时，你将会收到一个参数字典。</p>
</blockquote>
<p>让我们通过示例理解这个吧。声明一个函数，以 username 为参数，username 前面会有 <code>**</code>。</p>
<pre><code class="language-python">&gt;&gt;&gt; def user(**username):
...     print(username)
...
</code></pre>
<p>当我们调用 <code>user</code> 函数时，我们会收到一个字典。</p>
<pre><code class="language-python">&gt;&gt;&gt; user(username1="xyz",username2="abc")
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">{'username1': 'xyz', 'username2': 'abc'}
</code></pre>
<p>所以这里发生了什么呢？它看起来和 args 一摸一样，是不是？</p>
<p>不，并不是。在 args 中，你不能通过名字传值，因为它在元组中。这里我们得到的数据是字典，所以我们可以轻而易举地访问值。</p>
<p>考虑下这个示例：</p>
<pre><code class="language-python">&gt;&gt;&gt; def user(user_details):
...     print(user_details['username'])
...
</code></pre>
<p>调用我们的函数：</p>
<pre><code class="language-python">&gt;&gt;&gt; user(username="Sharvin",age="1000")
</code></pre>
<p>输出如下：</p>
<pre><code class="language-shell">Sharvin
</code></pre>
<h2 id="">作用域</h2>
<p>作用域（scope）定义了变量或函数的作用范围。Python 中有两种类型的作用域：全局（global）和局部（local）。</p>
<h3 id="">全局作用域</h3>
<p>在 Python 代码主体中创建的变量或函数被称为全局变量或全局函数，它们是全局作用域的一部分。例如：</p>
<pre><code class="language-python">&gt;&gt;&gt; greet = "Hello world"
&gt;&gt;&gt; def testing():
...     print(greet)
...
&gt;&gt;&gt; testing()
Hello world
</code></pre>
<p>我们在这里定义的是一个全局可用的变量 <code>greet</code>，因为它是在程序体内声明的。</p>
<h3 id="">局部作用域</h3>
<p>在函数内部创建的变量或函数被称为局部变量或局部函数，它们是局部作用域的一部分：</p>
<pre><code class="language-python">&gt;&gt;&gt; def testing():
...     greet = "Hello world"
...     print(greet)
...
&gt;&gt;&gt; testing()
Hello world
</code></pre>
<p>这里的 <code>greet</code> 是在 testing 函数内部创建的，它只能在这个函数内部使用。我们试着这代码主体内访问它，看看会发生什么：</p>
<pre><code class="language-python">&gt;&gt;&gt; print(greet)
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
NameError: name 'greet' is not defined
</code></pre>
<p><strong>记住：</strong> 在测试上述代码之前要重启 Python 控制台：先按 ctrl + d 停止程序，再使用 <code>python3</code> 命令启动 shell。因为第一个例子中将 <code>greet</code> 变量声明在全局作用域，所以在运行第二个示例时，它仍然在内存中可用。</p>
<p>由于 <code>greet</code> 并不是全局可用，所以我们会得到一个指出它未定义的错误。</p>
<h2 id="return-statement">Return 语句：</h2>
<p>到目前为止，我们的函数都非常简单：它们接收数据，进行处理，然后打印它们。但是在真实世界中，你需要函数将输出返回，以便它可以在不同操作中使用。</p>
<p><code>return</code> 语句可以达到这个目的。记住，return 语句只是函数和方法的一部分，它的语法非常简单。</p>
<pre><code class="language-python">&gt;&gt;&gt; def add(a, b):
...     return a + b
...
&gt;&gt;&gt; add(1,3)
4
</code></pre>
<p>我们将输出返回，而没有打印相加的结果。返回值也可以被存放在变量中。</p>
<pre><code class="language-python">&gt;&gt;&gt; sum = add(5,10)
&gt;&gt;&gt; print(sum)
15
</code></pre>
<h2 id="lambda-expression">Lambda 表达式</h2>
<p>有时候，你并不想在一个函数内执行太多的计算，这时写一个完整的函数就没什么意义了。要解决这个问题，我们可以使用 lambda 表达式或 lambda 函数。</p>
<p>那么什么是 lambda 表达式呢？它是一个匿名函数，表达式只能有一行。Lambda 表达式可以接收 n 个参数。</p>
<p>Lambda 表达式的语法如下：</p>
<pre><code class="language-python">variable = lambda arguments: operation
</code></pre>
<p>让我们通过一个示例进一步理解：</p>
<pre><code class="language-python">&gt;&gt;&gt; sum = lambda a: a + 10
</code></pre>
<p>我们在这里声明了一个变量 <code>sum</code>，它会被用来调用 lambda 函数。<code>a</code> 表示传递给函数的参数。</p>
<p>让我们调用一下我们的函数吧：</p>
<pre><code class="language-python">&gt;&gt;&gt; x(5)
15
</code></pre>
<h2 id="">列表推导式</h2>
<p>考虑这样这一种场景：你想要一个由平方数组成的列表。通常你会声明一个 <code>squares</code> 列表，然后在一个 for 循环中计算这些数字的平方。</p>
<pre><code class="language-python">&gt;&gt;&gt; squares = []
&gt;&gt;&gt; for x in range(10):
...     squares.append(x**2)
...
&gt;&gt;&gt; squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
</code></pre>
<p>虽然这是可行的，但是在列表推导式（list comprehension）的帮助下，我们用一行代码就可以实现：</p>
<p>实现的方式有两种，我们两个方法都理解一下。</p>
<pre><code class="language-python">&gt;&gt;&gt; squares = list(map(lambda x: x**2, range(10)))
&gt;&gt;&gt; squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
</code></pre>
<p>这里使用 <code>list</code> 构造器构造了一个列表，它里面是计算平方值的 lambda 函数。另一种方式也得到了同样的结果，如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; squares = list(x**2 for x in range(10))
&gt;&gt;&gt; squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
</code></pre>
<p>我更喜欢这种方式，因为它更简单、易懂。</p>
<p>如果我们有一个条件，想要一组相同的两个数字该怎么办呢？嗯，我们需要写两个 for 循环和一个 if 条件。</p>
<p>我们来看一下这该怎么写：</p>
<pre><code class="language-python">&gt;&gt;&gt; num_list = []
&gt;&gt;&gt; for i in range(10):
...     for j in range(10):
...             if i == j:
...                     num_list.append((i,j))
...
&gt;&gt;&gt; num_list
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]
</code></pre>
<p>这工作量还是挺大的，而且从可读性方面来看，代码更难理解了。</p>
<p>让我们用列表推导式实现它吧。</p>
<pre><code class="language-python">&gt;&gt;&gt; num_list = list((i,j) for i in range(10) for j in range(10) if i == j)

</code></pre>
<p>看见了吧，用一个表达式得到同样的结果是不是很容易？这就是列表生成式的强大之处。</p>
<h2 id="">面向对象编程</h2>
<p>Python 是一门多范式的编程语言，即它可以使用不同的方式解决同一个问题。其中的一种范式就是过程式（procedural）或函数式（functional）编程，代码结构就像一个食谱——一组以函数或代码块呈现的步骤。</p>
<p>解决这个问题的另一种方式就是创建类（class）和对象（object），这就是所谓的面向对象编程（OOP）。对象就是一组数据（变量）和方法的集合，方法操作这些数据。类是对象的蓝图。</p>
<p>理解面向对象编程的关键在于：以对象为中心——它们不仅代表数据，还代表程序的结构。</p>
<p>你可以为要处理的问题选择最适合的范式，在一个程序中混用不同的范式，也可以在程序的演进过程中从一种范式切换到另一种范式。</p>
<h3 id="">面向对象编程的优点</h3>
<ul>
<li>
<p><strong>继承（Inheritance）：</strong> 这是面向对象编程中最有用的概念之一。它指明子对象会拥有父对象所有的属性和行为。因此，继承允许我们定义一个类，这个类将继承另一个类的所有方法和属性。</p>
</li>
<li>
<p><strong>多态（Polymorphism）：</strong> 为了理解多态，我们可以将这个词划分为两部分。第一部分“poly”表示很多，而“morph”表示形成或形状。因此 polymorphism 表示一个任务可以以不同方式执行。</p>
<p>例如，你有一个 <code>animal</code> 类，并且所有的动物都能说话。但是它们说话的方式不同。这里的“说话”这个行为就是多态的，它依赖于具体的动物。所以，抽象的“动物”实际上并不“说话”，但是特定的动物（比如狗和猫）就有“说话”这个动作的具体实现。</p>
<p>多态意味着相同的函数名或方法名可以被用于不同的类型。</p>
</li>
<li>
<p><strong>封装（Encapsulation）：</strong> 在面向对象编程中，你可以限制对方法和变量的访问——我们可以把方法和变量设置成私有的。这就能防止数据被意外修改，这种方式被称为封装。</p>
</li>
</ul>
<p>首先，我们会理解类、对象和构造器。之后，我们会再次查看上述属性。如果你已经对类、对象和构造器有所了解，可以自由跳过。</p>
<h2 id="">类：</h2>
<p>Python 中一些可以直接使用的基本数据结构，比如数字、字符串和列表。这些都可以用来简单地表示名字、地方、价值，等等。</p>
<p>但是如果我们有更复杂的数据，该怎么办呢？如果这些数据有着重复的属性，我们可以做什么呢？</p>
<p>假设我们有一百只不同的动物，每只动物都有名字、年龄、腿，等等。如果我们想给每只动物再添加一个属性，或者再添加一只动物到这个列表中，该怎么办呢？要应付这种复杂的情况，我们需要使用类（class）。</p>
<p>根据 <a href="https://docs.python.org/3/tutorial/classes.html">Python 官方文档</a>：</p>
<blockquote>
<p>类提供了一种将数据数据和功能捆绑在一起的方法。创建一个新类意味着创建一种新的对象类型，从而允许创建一个该类型的新实例。</p>
</blockquote>
<p>每个类的实例可以拥有保存状态的属性，也可以有改变状态的（定义在类中的）方法。</p>
<p>类的语法：</p>
<pre><code class="language-python">class ClassName:

    &lt;expression-1&gt;
    .
    .
    .
    &lt;expression-N&gt;
</code></pre>
<p>我们使用 <code>class</code> 关键字定义一个类。我们将定义一个 <code>Car</code> 类。</p>
<pre><code class="language-python">class Car:
    pass
</code></pre>
<h2 id="">方法：</h2>
<p>方法（Method）看起来和函数一样，它们之间唯一的区别就是：方法依赖于对象。函数可以通过函数名调用，而方法必须通过它们的类引用调用。方法在类中定义。</p>
<p>在我们的示例中，创建了两个方法：一个是 engine，另一个是 wheel。这两个方法定义了汽车的可用部件。</p>
<p>下面这段程序会让我们对类有一个更好的理解：</p>
<pre><code class="language-python">&gt;&gt;&gt; class Car:
...     def engine(self):
...             print("Engine")
...

&gt;&gt;&gt; Car().engine()
Engine
</code></pre>
<p>在这里，我们通过 <code>Car()</code> 引用调用 <code>engine</code> 方法。</p>
<p>总而言之，类提供定义的蓝图，但并不提供任何真实内容。<code>Car</code> 类定义了引擎（engine），但它并不会声明一辆特定汽车的引擎是什么。引擎是通过对象声明的。</p>
<h2 id="">对象：</h2>
<p>对象（object）是类的实例。考虑上述汽车的例子，汽车就是我们的类，而 <code>toyota</code> 就是汽车的对象。我们可以创建多个对象的副本。每个对象都必须使用类进行定义。</p>
<p>创建对象的语法如下：</p>
<pre><code class="language-python">toyota = Car()

</code></pre>
<pre><code class="language-python">class Car:

    def engine(self):
        print("Engine")

    def wheel(self):
        print("Wheel")

toyota = Car()
</code></pre>
<p>上面的 <code>toyota = Car()</code> 就是一个 <strong>类对象</strong>。类对象支持两种类型的操作：属性引用和实例化。</p>
<p>类的实例化使用函数符号，实例化操作（“调用”类对象）会创建一个空对象。</p>
<p>现在我们可以使用 <code>toyota</code> 对象调用 <code>Car</code> 类的不同方法，让我们调用下方法 <code>engine</code> 和 <code>wheel</code> 吧。</p>
<p>打开你的编辑器，创建一个名为 <code>mycar.py</code> 的文件。将以下代码复制到该文件中：</p>
<pre><code class="language-python">class Car:

    def engine(self):
        print("Engine")

    def wheel(self):
        print("Wheel")

if __name__ == "__main__":
    toyota = Car()
    toyota.engine()
    toyota.wheel()
</code></pre>
<p>保存上述代码。现在让我们仔细看一看这个程序。</p>
<p>我们利用 <code>Car</code> 类创建了一个 <code>toyota</code> 对象。<code>toyota.engine()</code> 是一个方法对象，当一个方法对象被调用时，到底发生了什么？</p>
<p>调用 <code>toyota.engine()</code> 时并没有传递任何参数，但是如果你看一眼方法声明，你就会发现它有一个 <code>self</code> 参数。</p>
<p>你可能会因没有抛出错误而感到疑惑。其实每次我们在使用方法对象时， <code>toyota.engine()</code> 都会被转换成 <code>Car.engine(toyota)</code>。</p>
<p>使用以下命令运行程序。</p>
<pre><code class="language-shell">python mycar.py

</code></pre>
<p>你会得到以下输出：</p>
<pre><code class="language-shell">Engine
Wheel
</code></pre>
<h2 id="">构造器：</h2>
<p><code>__init__</code> 方法是 Python 中的构造器方法（constructor method），构造器方法用于初始化数据。</p>
<p>打开 Python shell，输入这个示例：</p>
<pre><code class="language-python">&gt;&gt;&gt; class Car():
...     def __init__(self):
...             print("Hello I am the constructor method.")
...

</code></pre>
<p>调用这个类得到的输出如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; toyota = Car()
Hello I am the constructor method.
</code></pre>
<p><strong>注意：</strong> 你永远都不必调用 <code>init()</code> 方法——它会在创建类实例时被自动调用。</p>
<h2 id="">实例属性：</h2>
<p>所有的类都有对象，所有的对象都有属性（attributes）。属性就是对象具有的性质。我们使用 <code>__init__()</code>方法声明一个对象的初始属性。</p>
<p>以汽车为例：</p>
<pre><code class="language-python">class Car():
    def init(self, model): 
        self.model = model  #实例属性
</code></pre>
<p>在我们的示例中，每个 <code>Car()</code> 都有一个特定的型号（model），因此实例属性对每个实例来说都是唯一的。</p>
<h2 id="">类属性：</h2>
<p>我们看到实例属性是针对每个对象来说的，但是类属性对所有的实例来说都是一样的。我们借助类属性看一下汽车的示例：</p>
<pre><code class="language-python">class Car():

    no_of_wheels = 4 #类属性
</code></pre>
<p>所以每辆汽车都可以有不同的型号，但是所有的汽车都只会有四个轮子。</p>
<h2 id="self">Self：</h2>
<p>现在我们来理解下 <code>self</code> 的含义，以及如何在面向对象编程中使用它。<code>self</code> 表示类的实例，我们可以通过这个关键字访问由构造器和类方法初始化的数据。</p>
<p>现在来看一个使用 <code>self</code> 的示例，我们在 <code>Car</code> 类中创建一个名为 <code>brand</code> 的方法。</p>
<p>在 <code>__init__</code> 方法中，我们会在实例化对象时传递汽车型号的名字，这个名字可以在类中的任何地方被访问，比如我们的 <code>self.model</code>。</p>
<p>打开名为 <code>mycar.py</code> 的文件，将旧代码换成这个代码：</p>
<pre><code class="language-python">class Car(): 

  def __init__(self, model): 
    self.model = model
  		
  def brand(self): 
    print("The brand is", self.model)  

if __name__ == "__main__":
  car = Car("Bmw")
  car.brand()
</code></pre>
<p>现在当我们使用以下命令运行上述程序时：</p>
<pre><code class="language-shell">python mycar.py
</code></pre>
<p>会得到以下结果：</p>
<pre><code class="language-shell">The brand is Bmw
</code></pre>
<p><strong>注意：</strong> <code>self</code> 是只是一个习惯，它并不是一个 Python 的关键词。方法中的 <code>self</code> 只是一个参数，我们可以使用另一个名字替换它。但是推荐使用 <code>self</code>，因为它会提高代码的可读性。</p>
<h2 id="">继承：</h2>
<p>继承指一个类继承了另一个类的属性。</p>
<p>被继承了属性的那个类称为基类（base class）。继承了另一个类的属性的那个类被称为派生类（derived class）。</p>
<p>继承可以被定义为父子关系。子类继承父类的性质，因此子类就是派生类，而父类就是基类。这里的性质指的是属性和方法。</p>
<p>派生类定义的语法如下：</p>
<pre><code class="language-python">class DerivedClassName(BaseClassName):
    &lt;statement-1&gt;
    .
    .
    .
    &lt;statement-N&gt;
</code></pre>
<p>注意到子类覆盖或扩展父类方法的属性和行为 z 这一点很重要。这就是说，子类继承父类所有的属性和行为，但是子类又可以声明不同的行为。</p>
<p>最基本的类类型是 object，它通常被所有的其它类作为父类继承。我们来修改一下之前的示例，理解继承是如何工作的。</p>
<p>创建一个名为 <code>vehicle</code> 的基类：</p>
<pre><code class="language-python">class Vehicle:
    def __init__(self, name):
        self.name = name
    
    def getName(self):
        return self.name
</code></pre>
<p>我们已经创建了一个 <code>Vehicle</code> 类，使用 <code>self.name</code> 实例化了一个构造器，我们将在 <code>getName</code> 方法中使用 <code>self.name</code>。每次调用这个方法时，它都会返回对象初始化时传入的 <code>name</code>。</p>
<p>现在创建一个子类 <code>Car</code>：</p>
<pre><code class="language-python">class Vehicle:
    def __init__(self, name):
        self.name = name
    
    def getName(self):
        return self.name

class Car(Vehicle):
  pass
</code></pre>
<p><code>Car</code> 是 <code>Vehicle</code> 的一个子类，它继承了父类所有的方法和属性。</p>
<p>现在在子类 <code>Car</code> 中使用父类 <code>Vehicle</code> 的方法和属性。</p>
<pre><code class="language-python">class Vehicle:

    def __init__(self, name, color='silver'):
        self.name = name
        self.color = color
    
    def get_name(self):
        return self.name
    
    def get_color(self):
        return self.color

class Car(Vehicle):
  pass

audi = Car("Audi r8")
print("The name of our car is", audi.get_name(), "and color is", audi.get_color())
</code></pre>
<p>理解一下我们在这里干了啥。</p>
<p>我们声明了一个名为 <code>Vehicle</code> 的类，它的构造器将名字作为参数，而颜色有一个默认参数。</p>
<p>类中有两个方法：<code>get_name</code> 返回名字，而 <code>get_color</code> 返回颜色。我们实例化了一个对象，传递了一个汽车名。</p>
<p>在这里，你会发现我们在子类声明中使用了父类的方法。</p>
<p>使用以下命令运行上述程序：</p>
<pre><code class="language-shell">python mycar.py
</code></pre>
<p>输出：</p>
<pre><code>The name of our car is Audi r8 and color is silver

</code></pre>
<p>我们也重写了一个父类的方法或属性。在以上示例中，我们定义交通工具的颜色为银色，但是如果我们的汽车是黑色的呢？</p>
<p>对于每个子类，我们不能直接在它的父类中进行修改，这就有了重写机制。</p>
<pre><code class="language-python">class Vehicle:

    def __init__(self, name, color='silver'):
        self.name = name
        self.color = color
    
    def get_name(self):
        return self.name
    
    def get_color(self):
        return self.color

class Car(Vehicle):

    def get_color(self):
        self.color = 'black'
        return self.color

audi = Car("Audi r8")
print("The name of our car is", audi.get_name(), "and color is", audi.get_color()
</code></pre>
<p>如你所见，我们并没有实例化一个构造器。因为子类 <code>Car</code> 只是使用来自 <code>Vehicle</code> 类的属性，而它早就继承这些属性了。所以在这种场景下，没有必要重新实例化这些属性。</p>
<p>运行程序，输出如下：</p>
<pre><code class="language-shell">The name of our car is Audi r8 and color is black

</code></pre>
<h2 id="super">Super：</h2>
<p><code>super()</code> 返回一个父类的临时对象，允许我们调用父类的方法。</p>
<p>直接使用 <code>super()</code> 调用已有的方法让我们免于在子类中重写那些方法，允许我们用最小的代码改动替换父类。因此 <code>super</code> 扩展了继承方法的功能。</p>
<p>让我们使用 <code>super()</code> 对汽车示例进行扩展吧。我们将用父类 <code>Vehicle</code> 中的 <code>brand_name</code> 和 <code>color</code> 重新实例化一个构造器。现在在子类（<code>Car</code>）中使用 <code>super</code> 调用父类的这个构造器，我们将创建一个 <code>get_description</code> 方法，它从 <code>Car</code> 类返回 <code>self.model</code> 并从 <code>Vehicle</code> 类返回 <code>self.brand_name</code> 和 <code>self.color</code>。</p>
<pre><code class="language-python">class Vehicle:
 
    def __init__(self, brand_name, color):
        self.brand_name = brand_name
        self.color = color
 
    def get_brand_name(self):
        return self.brand_name
 
class Car(Vehicle):
 
    def __init__(self, brand_name, model, color):  
        super().__init__(brand_name, color)       
        self.model = model
 
    def get_description(self):
        return "Car Name: " + self.get_brand_name() + self.model + " Color:" + self.color
 
c = Car("Audi ",  "r8", " Red")
print("Car description:", c.get_description())
print("Brand name:", c.get_brand_name())
</code></pre>
<p>运行上述程序，输出如下：</p>
<pre><code class="language-shell">Car description: Car Name: Audi r8 Color: Red
Brand name: Audi
</code></pre>
<h2 id="">多重继承：</h2>
<p>多重继承（multiple inheritance）是指一个类从多个父类继承方法和属性。它允许我们在派生类或子类中使用多个基类或父类的性质。</p>
<p>多继承的通用语法如下：</p>
<pre><code class="language-python">class DerivedClassName(Base1, Base2, Base3):
    &lt;statement-1&gt;
    .
    .
    .
    &lt;statement-N&gt;
</code></pre>
<p>让我们使用多继承对交通工具这个示例进行扩展吧。在这个示例中我们会创建三个类：<code>Vehicle</code>、<code>Cost</code> 和 <code>Car</code>。</p>
<p><code>Vehicle</code> 类 和 <code>Cost</code> 会成为父类。<code>Vehicle</code> 类表示通用属性，而 <code>Cost</code> 类表示价值。</p>
<p>因为 <code>Car</code> 有一个通用属性和价值，所以它将会有两个父类。因此我们将继承多个父类。</p>
<pre><code class="language-python">class Vehicle:

    def __init__(self, brand_name):
        self.brand_name = brand_name
    
    def get_brand_name(self):
        return self.brand_name


class Cost:		

    def __init__(self, cost):
        self.cost = cost
    
    def get_cost(self):
        return self.cost

 
class Car(Vehicle, Cost):	

    def __init__(self, brand_name, model, cost): 
        self.model = model 
        Vehicle.__init__(self, brand_name) 
        Cost.__init__(self, cost) 

    def get_description(self):
        return self.get_brand_name() + self.model + " is the car " + "and it's cost is " + self.get_cost()
		
c = Car("Audi ",  "r8", "2 cr")
print("Car description:", c.get_description())
</code></pre>
<p>在上述程序中，你会发现有一个东西与这篇教程中的其它程序都不同，我在 <code>Car</code> 类的构造器中使用了 <code>Vehicle.__init(self, brand_name)</code>。这是调用父类属性的一种方式，另一种方式就是我在上面提到的 <code>super</code>。</p>
<p>运行程序，结果如下：</p>
<pre><code class="language-python">Car description: Audi r8 is the car and it's cost is 2 cr

</code></pre>
<p>尽管多重继承可以被高效使用，但是也需要保持谨慎，避免程序变得模棱两可，防止程序对于其它程序员难于理解。</p>
<h2 id="">多态：</h2>
<p>多态一词表示具有很多种形式。在编程中，多态表示同一个函数名（但是不同的函数签名）被用于不同的类型。</p>
<p>让我们用多态扩展汽车程序吧。我们将会创建两个类：<code>Car</code> 和 <code>Bike</code>。两个类都有共同的方法或函数，但是它们会打印不同的数据。程序本身很容易理解：</p>
<pre><code class="language-python">class Car: 

    def company(self): 
        print("Car belongs to Audi company.")
   
    def model(self): 
        print("The Model is R8.") 
   
    def color(self): 
        print("The color is silver.") 
   
class Bike: 

    def company(self): 
        print("Bike belongs to pulsar company.") 
   
    def model(self): 
        print("The Model is dominar.") 
   
    def color(self): 
        print("The color is black.") 
  
def func(obj): 
    obj.company() 
    obj.model() 
    obj.color() 
   
car = Car() 
bike = Bike() 
   
func(car) 
func(bike)
</code></pre>
<p>运行以上代码，结果如下：</p>
<pre><code class="language-shell">Car belongs to Audi company.
The Model is R8.
The color is silver.
Bike belongs to pulsar company.
The Model is dominar.
The color is black.
</code></pre>
<h2 id="">封装：</h2>
<p>在大多数面向对象编程中，我们都可以限制对方法和变量的访问。这能防止数据被意外修改，也被称为封装。</p>
<p>让我们在汽车示例中使用封装吧。假设我们有一个绝密的引擎。在第一个示例中，我们将使用 <strong>私有变量</strong> 隐藏引擎。在第二个示例中，我们将使用 <strong>私有方法</strong> 对引擎进行隐藏。</p>
<p><strong>示例 1：</strong></p>
<pre><code class="language-python">cclass Car:

  def __init__(self): 
    self.brand_name = 'Audi '
    self.model = 'r8'
    self.__engine = '5.2 L V10'
    
  def get_description(self):
        return self.brand_name + self.model + " is the car"
  
c = Car()
print(c.get_description)
print(c.__engine)
</code></pre>
<p>在这个示例中，<code>self.__engine</code> 是私有属性。当我们运行这个程序时，就会得到以下结果。</p>
<pre><code class="language-shell">Audi r8 is the car
AttributeError: 'Car' object has no attribute 'engine'
</code></pre>
<p>我们得到了一个错误：<code>Car</code> 对象没有 <code>_engine</code> 属性，因为它是一个私有对象。</p>
<p><strong>示例 2：</strong></p>
<p>我们也可以通过在方法名前面加上 <code>__</code> 定义私有方法。以下就是一个定义私有方法的示例。</p>
<pre><code class="language-python">class Car:

  def __init__(self):
      self.brand_name = 'Audi '
      self.model = 'r8'

  def __engine(self):
      return '5.2 L V10'

  def get_description(self):
      return self.brand_name + self.model + " is the car"
    
    
c = Car()
print(c.get_description())
print(c.__engine()) 
</code></pre>
<p>示例中的 <code>def __engine(self)</code> 是一个私有方法。运行程序，结果如下：</p>
<pre><code class="language-shell">Audi r8 is the car
AttributeError: 'Car' object has no attribute '__engine'
</code></pre>
<p>假设现在我们想访问私有属性或私有方法，我们可以这么做：</p>
<pre><code class="language-python">class Car:

  def __init__(self):
      self.brand_name = 'Audi '
      self.model = 'r8'
      self.__engine_name = '5.2 L V10'

  def __engine(self):
      return '5.2 L V10'

  def get_description(self):
      return self.brand_name + self.model + " is the car"
    
    
c = Car()
print(c.get_description())
print("Accessing Private Method: ", c._Car__engine()) 
print("Accessing Private variable: ", c._Car__engine_name)
</code></pre>
<p>程序的输出如下：</p>
<pre><code class="language-shell">Audi r8 is the car
Accessing Private Method:  5.2 L V10
Accessing Private variable:  5.2 L V10
</code></pre>
<p>封装使你可以更好地控制代码中的耦合程度，允许你在不影响其它部分的情况下修改类的实现。</p>
<h2 id="">装饰器：</h2>
<p>假设你需要扩展多个函数的功能，你会怎么做？</p>
<p>一种方式是进行函数调用，你可以在函数内进行处理。对三十到四十个函数调用进行修改，还要记住应该把调用放在何处，这是一项很棘手额工作。不过，Python 给你提供了一种更加优雅的方式——装饰器（decorator）。</p>
<p>什么是装饰器？装饰器就是一个函数，它接受一个函数并扩展其功能，整个过程不涉及对原函数的显式修改。我觉得你仍然不太明白装饰器是什么，别急，我们有一个示例对它进行解释。</p>
<p>让我们通过示例理解装饰器吧。有两种写装饰器的方法。</p>
<h3 id="1">方法 1</h3>
<p>我们声明一个装饰器函数，并将我们期望的函数作为参数传入。在装饰器函数内部，我们写一个包装函数执行操作并将其返回。</p>
<pre><code class="language-python">&gt;&gt;&gt; def my_decorator(func):
...     def wrapper():
...             print("Line Number 1")
...             func()
...             print("Line Number 3")
...     return wrapper
...
&gt;&gt;&gt; def say_hello():
...     print("Hello I am line Number 2")
...
</code></pre>
<p>要调用这个函数，我们将 <code>say_hello</code> 赋给装饰器作为参数。</p>
<pre><code class="language-python">&gt;&gt;&gt; say_hello = my_decorator(say_hello)
</code></pre>
<p>我们还可以使用 <code>say_hello</code> 检查引用，结果会告诉我们它已经被 <code>my_decorator</code> 函数包装。</p>
<pre><code class="language-python">&lt;function my_decorator.&lt;locals&gt;.wrapper at 0x10dc84598&gt;
</code></pre>
<p>调用一下 <code>say_hello</code> 函数：</p>
<pre><code class="language-python">&gt;&gt;&gt; say_hello()
Line Number 1
Hello I am line Number 2
Line Number 3
</code></pre>
<p>“Hello I am line Number 2”魔术般地被打印在“Line Number 1”和“Line Number 3”之间，因为这个函数调用就在那里执行的。</p>
<p>方法 1 很笨重，因此很多人更喜欢使用另一种方式。</p>
<h3 id="2">方法 2</h3>
<p>这里我们的装饰器装饰仍然不变，但是我们改变了调用被赋给装饰器的方式。每个需要装饰器的函数都用 <code>@decorator_name</code> 包裹自己。</p>
<pre><code class="language-python">&gt;&gt;&gt; def my_decorator(func):
...     def wrapper():
...             print("Line Number 1")
...             func()
...             print("Line Number 3")
...     return wrapper
...
&gt;&gt;&gt; @my_decorator
... def say_hello():
...     print("Hello I am line Number 2")
...
&gt;&gt;&gt; say_hello()
</code></pre>
<p>输出不变：</p>
<pre><code class="language-shell">Line Number 1
Hello I am line Number 2
Line Number 3
</code></pre>
<p>装饰器是一个非常强大的工具，它被用在下列开发场景中：</p>
<ul>
<li>日志设置</li>
<li>配置设置</li>
<li>错误捕获</li>
<li>为所有的函数和类扩展通用功能</li>
</ul>
<h2 id="">异常：</h2>
<p>在学习各种语法的过程中，我们也遇到了各种各样的错误。那些错误因为语法问题而出现。但是在真实应用中，错误（通常也称为 bug）并不只是由语法问题引起的，也可能是网络或其他原因。</p>
<p>我们使用 Try-Except 处理这些问题。在 <code>try</code> 块中，写我们想要执行的表达式，而在 <code>except</code> 块中，我们捕获错误。Try-Except 块形式如下：</p>
<pre><code class="language-python">try:
    expression
except:
    catch error
</code></pre>
<p>让我们通过示例来理解吧：</p>
<pre><code class="language-python">&gt;&gt;&gt; try:
...     print(value)
... except:
...     print("Something went wrong")
...
</code></pre>
<p>我们尝试打印出 value 变量，但是它并没有被定义。所以我们得到了以下结果：</p>
<pre><code class="language-shell">Something went wrong
</code></pre>
<p>你可能在想，“something went wrong”这一行并没有多大帮助。所以我们要怎样才能知道是什么错了呢？</p>
<p>我们可以把异常打印出来，然后用它找出是什么出错了。我们在示例中试一下吧：</p>
<pre><code class="language-python">&gt;&gt;&gt; try:
...     print(value)
... except Exception as e:
...     print(e)
...
</code></pre>
<p>结果为：</p>
<pre><code class="language-shell">name 'value' is not defined
</code></pre>
<p>哇！太神奇了。它提示我“value”没有被定义。</p>
<p>Python 也提供了一个名为 <code>raise</code> 的工具。假如你不希望某个条件发生，你想在它发生的时候把它抛出来。在这种情况下，你可以使用 <code>raise</code>。示例如下：</p>
<pre><code class="language-python">&gt;&gt;&gt; i = 5
&gt;&gt;&gt; if i &lt; 6:
...     raise Exception("Number below 6 are not allowed")
...
</code></pre>
<p>我们得到的结果如下：</p>
<pre><code class="language-shell">Traceback (most recent call last):
  File "&lt;stdin&gt;", line 2, in &lt;module&gt;
Exception: Number below 6 are not allowed
</code></pre>
<p>异常有很多子类型，我推荐你阅读 <a href="https://docs.python.org/3/tutorial/errors.html#errors-and-exceptions">Python 文档</a>，进一步理解它们。</p>
<h2 id="">包的导入：</h2>
<p>你已经学完了 Python 的基础知识，现在已经做好构建出色应用程序的准备了。但是，等一等，我们还漏掉了一些重要的主题。</p>
<p>如果没有包的导入，你将被迫在一个文件里写下所有的代码。想象一下这是多么的糟糕。</p>
<p>创建两个文件，分别命名为 <code>mani.py</code> 和 <code>hello.py</code>。记住，两个文件需要在同一个目录中。</p>
<p>将以下代码复制到 <code>hello.py</code>中：</p>
<pre><code class="language-python">def say_hello():
    print("Hello world")
</code></pre>
<p>将以下代码复制到 <code>main.py</code> 中：</p>
<pre><code class="language-python">import hello

if __name__ == "__main__":
    hello.say_hello()
</code></pre>
<p>我们在 <code>hello.py</code> 中声明了一个 <code>say_hello()</code> 函数，它打印“Hello world”。在 <code>main.py</code> 中，你会看到一个 import 语句。我们导入 hello 模块，并从那个模块调用 <code>say_hello</code> 函数。</p>
<p>使用以下命令运行程序：</p>
<pre><code class="language-shell">➜ python main.py
</code></pre>
<p>输出：</p>
<pre><code class="language-shell">Hello world
</code></pre>
<p>现在我们来理解一下如何导入另一个目录中的模块。</p>
<p>我们先创建一个名为“data”的目录，然后将 <code>hello.py</code> 移动到该目录下。</p>
<p>打开 <code>main.py</code>，修改之前的 import 语句：</p>
<pre><code class="language-python">from data import hello

if __name__ == "__main__":
    hello.say_hello()
</code></pre>
<p>从一个目录导入的方式有两种：</p>
<ul>
<li>方法 1：<code>from data import hello</code></li>
<li>方法 2：<code>import data.hello</code></li>
</ul>
<p>我更喜欢方法 1，因为它更易读。你可以选择适合自己的导入方式。</p>
<p>使用以下命令运行我们的应用：</p>
<pre><code class="language-shell">➜ python main.py
</code></pre>
<p>出现了一个错误，为什么会这样呢？我们没做错，仔细检查一下错误吧：</p>
<pre><code class="language-shell">Traceback (most recent call last):
  File "main.py", line 1, in &lt;module&gt;
    from data import hello
ImportError: No module named data
</code></pre>
<p>Python 告诉我们它没有识别出名为“data”的模块。要解决这个问题，我们需要在“data”目录中创建一个 <code>__init__.py</code>。文件内容留空，再次运行程序，你会得到以下结果：</p>
<pre><code class="language-shell">Hello world
</code></pre>
<p>默认情况下，Python 不把目录当作模块对待。要通知 Python 将某个目录视为模块，需要有 <code>__init__.py</code>。</p>
<h2 id="json-handling">JSON 处理：</h2>
<p>如果你之前从事过 web 开发或应用开发，你可能会知道所有的 API 调用都以 JSON 格式进行。虽然 JSON 看起来和 Python 中的字典很像，但是它完全不同。</p>
<p>Python 提供了一个内置的 <code>json</code> 包，用于处理 JSON。要使用这个包，我们需要先导入它：</p>
<pre><code class="language-python">import json
</code></pre>
<p>这个库提供了两个处理 JSON 的方法，我们逐一进行理解：</p>
<h3 id="jsonloads">JSON loads：</h3>
<p>如果你想把 JSON 字符串转换回字典，就需要使用 <code>load</code> 方法。打开 Python shell，复制-粘贴以下代码：</p>
<pre><code class="language-python">&gt;&gt;&gt; import json
&gt;&gt;&gt; json_string = '{ "user_name":"Sharvin", "age":1000}' #JSON 字符串
&gt;&gt;&gt; type(json_string)
&lt;class 'str'&gt;
&gt;&gt;&gt; data = json.loads(json_string)
&gt;&gt;&gt; type(data)
&lt;class 'dict'&gt;
&gt;&gt;&gt; data
{'user_name': 'Sharvin', 'age': 1000}
</code></pre>
<h3 id="jsondumps">JSON dumps：</h3>
<p>现在，让我们用 <code>dumps</code> 方法将数据转换回 JSON 字符串格式吧。</p>
<pre><code class="language-python">&gt;&gt;&gt; jsonString = json.dumps(data)
&gt;&gt;&gt; type(jsonString)
&lt;class 'str'&gt;
&gt;&gt;&gt; jsonString
'{"user_name": "Sharvin", "age": 1000}'
</code></pre>
<p>若想学习更多关于 JSON 操作的支持，可以前往 <a href="https://docs.python.org/3/library/json.html">Python 文档</a>。</p>
<h2 id="">说完了</h2>
<p>说完了！我希望你现在已经理解 Python 的基础知识。祝贺你！这是一个巨大的成就。</p>
<p>欢迎反馈。如果你想了解任何其它主题的内容，你可以在 Twitter 上发送主题名字并带上我的 Twitter handle（<strong>@sharvinshah26</strong>）。</p>
<blockquote>
<p>随时通过 <a href="https://twitter.com/sharvinshah26">Twitter</a> 和 <a href="https://github.com/Sharvin26">Github</a> 与我联系。</p>
</blockquote>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-ultimate-guide-to-python-from-beginner-to-intermediate-to-pro/">The Ultimate Guide to Python: How to Go From Beginner to Pro</a>，作者：<a href="https://www.freecodecamp.org/news/author/sharvin/">Sharvin Shah</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 白话 OSI 七层网络模型 ]]>
                </title>
                <description>
                    <![CDATA[ 本文讲的是开放网络互联（OSI，Open Systems Interconnection）模型以及网络体系结构中的七个层次。 OSI 模型是一个描述网络功能的概念框架。简单来说，OSI 模型标对计算机系统彼此之间发送信息的方式进行了标准化。 学习网络有点像学习一门语言——有很多标准，又有一些例外。因此，真正理解 “OSI 模型不是一组规则”非常重要，它是一个用于理解网络如何运作的工具。 当你学会了 OSI 模型，你不仅能进一步理解和欣赏这个被我们称之为“因特网”的宏大实体，还能更轻松流畅地排查网络问题。 向因特网致敬！ 预备知识 虽然你无需具备任何编程或网络方面的经验就能理解本文，但是你需要：  * 基本熟悉常见的网络术语（下面会解释）  * 好奇事物如何运作的 :) 学习目标 你将从本文学到：  1. 什么是 OSI 模型  2. 七层模型中各层的用途  3. 七层模型中各层可能出现的问题  4. TCP/IP 模型与 OSI 模型的区别 常见网络术语 这里是一些常见的网络术语，为了充分理解本文，你应该熟悉它们。我会在接下来谈论 OSI 各层的时候使用这些术语。  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/osi-model-networking-layers/</link>
                <guid isPermaLink="false">6018bbbf6183a7054015635e</guid>
                
                    <category>
                        <![CDATA[ 计算机网络 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Tue, 02 Feb 2021 02:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/network-3537401_1920.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文讲的是开放网络互联（OSI，Open Systems Interconnection）模型以及网络体系结构中的七个层次。</p>
<p>OSI 模型是一个描述网络功能的概念框架。简单来说，OSI 模型标对计算机系统彼此之间发送信息的方式进行了标准化。</p>
<p>学习网络有点像学习一门语言——有很多标准，又有一些例外。因此，真正理解 “OSI 模型不是一组规则”非常重要，它是一个用于理解网络如何运作的工具。</p>
<p>当你学会了 OSI 模型，你不仅能进一步理解和欣赏这个被我们称之为“因特网”的宏大实体，还能更轻松流畅地排查网络问题。</p>
<p>向因特网致敬！</p>
<h2 id="">预备知识</h2>
<p>虽然你无需具备任何编程或网络方面的经验就能理解本文，但是你需要：</p>
<ul>
<li>基本熟悉常见的网络术语（下面会解释）</li>
<li>好奇事物如何运作的 :)</li>
</ul>
<h2 id="">学习目标</h2>
<p>你将从本文学到：</p>
<ol>
<li>什么是 OSI 模型</li>
<li>七层模型中各层的用途</li>
<li>七层模型中各层可能出现的问题</li>
<li>TCP/IP 模型与 OSI 模型的区别</li>
</ol>
<h2 id="">常见网络术语</h2>
<p>这里是一些常见的网络术语，为了充分理解本文，你应该熟悉它们。我会在接下来谈论 OSI 各层的时候使用这些术语。</p>
<h3 id="">节点</h3>
<p>节点（node）是连接到网络的物理电子设备，比如电脑、打印机、路由器等等。如果配置正确的话，节点可以在网络上进行信息的收发。</p>
<p>节点可以彼此相邻，其中的节点 A 可以直接连接到节点 B。节点之间也可以有中间节点，例如节点 A 和节点 B 之间可以放置一个交换机或路由器。</p>
<p>通常，路由器将网络连接到因特网，而交换机运行在网络内部，促进内网通信。<a href="https://www.themillergroup.com/differences-hubs-switches-routers/">了解更多有关集线器、交换机和路由器的信息</a>。</p>
<p>举个例子：</p>
<figure>
    <img src="https://chinese.freecodecamp.org/news/content/images/2021/11/1-Router-Image.jpeg" width="1031" height="416" alt="1-Router-Image" loading="lazy">
    <figcaption><a href="https://learning.oreilly.com/library/view/the-illustrated-network/9780128110287/xhtml/chp001.xhtml">来源</a></figcaption>
</figure>
<p><em>对于我们当中那些挑剔的人来说（没错，我发现你了），<strong>主机（host）</strong> 将是你在网络中遇到的另一个术语。我将把它定义为一种需要具有 IP 地址的节点。所有的主机都是节点，但是并不是所有的节点都是主机。如果你不赞同，带着愤怒 Tweet 我吧。</em></p>
<h3 id="">链路</h3>
<p>链路（link）连接网络中的节点，它可以是有线的，比如以太网，也可以是无线的，比如 WiFi。</p>
<p>链路要么是点对点的（节点 A 与节点 B 相连），要么是多点的（节点 A 与节点 B 和节点 C 相连）。</p>
<p>我们也可以在谈论信息传输时将其描述成一对一与一对多的关系。</p>
<h3 id="">协议</h3>
<p>协议（protocol）是一组相互商定的规则，允许网络中的两个节点交换数据。</p>
<blockquote>
<p>“协议定义了管理通信过程中语法（可通信的内容）、语义（如何通信）以及同步（何时通信以及通信的速度）的规则。协议可以由硬件、软件或二者的组合实现。协议可以由任何人创建，但是最被广泛采纳的协议都是基于标准的。” —— The Illustrated Network</p>
</blockquote>
<p>有线和无线链路都可以有协议。</p>
<p>虽然任何人都可以创建协议，但是基于因特网组织（例如，因特网工程任务组（IETF，Internet Engineering Task Force））发布的标准的协议通常是最被广泛采纳的。</p>
<h3 id="">网络</h3>
<p>网络（network）是一组计算机、打印机或任何其它想要共享数据的设备的通用术语。</p>
<p>网络的类型包括：LAN、HAN、CAN、MAN、WAN、BAN 或 VPN。你觉得我只是随便用 <em>can</em> 一词来押韵吗？才不是呢——这些都是真实的网络类型。从<a href="https://www.c1c.net/blog/network-101/">这里</a>了解更多。</p>
<h3 id="">拓扑</h3>
<p>拓扑（topology）描述的是节点和链路如何在网络配置中组合在一起，通常用图描述。这里是一些常见的网络拓扑类型：</p>
<figure>
    <img src="https://chinese.freecodecamp.org/news/content/images/2021/11/2-Network-Topology-Types.png" alt="What is Network Topology? Best Guide to Types &amp; Diagrams - DNSstuff" width="1024" height="536" loading="lazy">
    <figcaption><a href="https://www.dnsstuff.com/what-is-network-topology">来源</a> + <a href="https://www.geeksforgeeks.org/types-of-network-topology/">了解更多网络拓扑</a></figcaption>
</figure>
<p><em>网络由节点、节点之间的链路和管理节点间数据传输的协议组成。</em></p>
<p>无论网络的规模和复杂度如何，你都可以通过学习 OSI 模型和七层网络来理解所有在计算机网络中发生的事情。</p>
<h1 id="osi">什么是 OSI 模型？</h1>
<p>OSI 模型由七层网络组成。</p>
<p>首先，层（layer）是什么？</p>
<figure>
    <img src="https://chinese.freecodecamp.org/news/content/images/2021/11/3-Dragon-Lair.jpeg" alt="洞穴、龙穴、群山" width="1280" height="960" loading="lazy">
    <figcaption><a href="https://pixabay.com/photos/cave-dragon-s-lair-mountains-1766835/">来源</a></figcaption>
</figure>
<p>噢，巢穴（lair）。</p>
<p>不，层——而不是 <em>巢穴</em>。这里没有龙。</p>
<p><em>层是对网络上的功能和行为进行分类和分组的一种方式。</em></p>
<p>在 OSI 模型中，层的组织结构从最具形态和最物理到不太有形，虚拟但更接近最终用户。</p>
<p>每一层都 <em>抽象</em> 低层的功能，直到最高层为止。最终用户是看不到所有其它层的所有细节和内部运作的。</p>
<p>如何记住所有层的名字呢？很简单。</p>
<blockquote>
<p>请不要把暗号告诉任何人（Please Do Not Tell the Secret Password to Anyone）。</p>
</blockquote>
<ul>
<li><strong>Please</strong> | 物理层（Physical Layer）</li>
<li><strong>Do</strong> | 数据链路层（Data Link Layer）</li>
<li><strong>Not</strong> | 网络层（Network Layer）</li>
<li><strong>Tell</strong> （the）| 传输层（Transport Layer）</li>
<li><strong>Secret</strong> | 会话层（Session Layer）</li>
<li><strong>Password</strong> （to）| 表示层（Presentation Layer）</li>
<li><strong>Anyone</strong> | 应用层（Application Layer）</li>
</ul>
<p><em>要牢记：虽然某些技术（比如协议）在逻辑上比起其它层来说可能“属于”某一层，但并非所有的技术都完全契合 OSI 模型中的单个层。例如，以太网（Ethernet）、802.11（Wifi）和地址解析协议（ARP，Address Resolution Protocol）程序在不只一层上工作。</em></p>
<p>OSI 只是一个模型，一个工具，并不是一组规则。</p>
<h2 id="osi">OSI 第一层</h2>
<p>第一层是 <strong>物理层</strong>。第一层中有很多技术——从物理网络设备、布线到电缆如何连接到设备之间的一切。另外，如果我们不需要电缆，那么信号的类型和传输方式是什么（例如，无线宽带）。</p>
<p>我没有列出第一层中的各种技术，而是为这些技术创建了一个更大的分类。我鼓励读者进一步学习每一种分类：</p>
<ul>
<li><strong>节点（设备）和网络硬件。</strong> 设备包括集线器、中继器、路由器、计算机、打印机，等等。这些设备内的硬件包括天线、放大器、网卡（NIC，Network Interface Card），等等。</li>
<li><strong>设备接口机制。</strong> 电缆如何连接到某个设备，以及连接到设备上的哪个地方（电缆连接器和设备插座）？连接器的大小和形状如何，它有多少个引脚？决定引脚处于活动状态还是非活动状态的东西是什么？</li>
<li><strong>功能和程序逻辑。</strong> 连接器中每个引脚的功能是什么——发送还是接收？决定事件顺序，以便节点能够开始与第二层上的另一个节点通信的程序逻辑是什么？</li>
<li><strong>电缆协议和规范。</strong> 以太网（CAT）、USB、<a href="https://www.centurylink.com/home/help/internet/what-is-DSL.html">数字用户线（DSL，Digital Subcriber Line）</a>等。规范包括最大电缆长度、调制技术、无线电规范、线路编码和位同步（下文还有更多）。</li>
<li><strong>电缆类型。</strong> 选择有屏蔽或非屏蔽双绞线、非双绞线、同轴电缆等。<a href="https://www.computernetworkingnotes.com/networking-tutorials/network-cable-types-and-specifications.html">从这里了解更多电缆类型</a>。</li>
<li><strong>信号类型。</strong> 基带一次一个比特流，就像铁路一样——只支持单向。宽带同时包含多个比特流，就像双向高速公路一样。</li>
<li><strong>信号传输方法（可能是有线的或无线的）。</strong> 选择包括电（以太网）、光（光纤网络、光纤）、无线电波（802.11 WiFi，a/b/g/n/ac/ax 变种或蓝牙）。如果是无线的话，则要考虑频率：2.5 GHz 还是 5 GHz。如果是有线或以太网的话，则还要考虑网络标准，例如 100BASE-T 和相关标准。</li>
</ul>
<p>第一层的数据单元是比特（bit）。</p>
<p>比特是可传输数字信息的最小单元。比特是二进制的，要么为 0 要么为 1。字节（byte）由八个比特组成，用于表示单个字符，比如字母、数字或符号。</p>
<p>根据硬件设备支持的数据速率（传输速率，每秒或每毫秒的比特数量），比特被发送到硬件设备或从设备发出。这个过程是同步的，从而保持单位时间内发送和接收比特的数量相等（这被称为比特同步）。比特的传输方式由信号的传输方式决定。</p>
<p>节点可以发送比特、接收比特，或者收发兼顾。如果节点只能收或只能发，那么该节点采用的就是单工模式。如果节点既可以收又可以发，那么该节点采用的就是双工模式。如果一个节点可以同时进行收发操作，那么它就是全双工的，否则就是半双工的。</p>
<p>最初的以太网是半双工的。如果采用了正确的设备，现在也可以选择全双工的以太网。</p>
<h3 id="">如何排查第一层中的问题</h3>
<p>这里是第一层中要当心的一些问题：</p>
<ul>
<li>电缆失效，例如电线损坏或连接器损坏</li>
<li>网络硬件设备故障，例如电路损坏</li>
<li>东西正被拔出（我们都遇到过……）</li>
</ul>
<p>如果第一层出了问题，第一层以上的任何东西都不会正常工作。</p>
<h3 id="tldr">TL;DR</h3>
<p><em>第一层包含的是基础设施，它让网络通信变成可能。</em></p>
<p><em>它定义了用于激活、维护和停用网络设备之间的物理连接的电气、机械、程序和功能规范。</em>——<a href="https://learning.oreilly.com/videos/wireshark-for-packet/9781839212352/9781839212352-video3_11">来源</a></p>
<p>有趣的事实：深海通信电缆在全世界传输数据。这张地图会让你大开眼界：<a href="https://www.submarinecablemap.com/">https://www.submarinecablemap.com/</a>。</p>
<p>因为你已经坚持到这儿了，所以送你一只考拉：</p>
<figure>
    <img src="https://chinese.freecodecamp.org/news/content/images/2021/11/4-Koala.jpeg" alt="考拉、自然、动物、爪子、澳大利亚、幼仔" width="1280" height="850" loading="lazy">
    <figcaption><a href="https://pixabay.com/photos/koala-nature-animals-paws-630117/">来源</a></figcaption>
</figure>
<h2 id="osi">OSI 第二层</h2>
<p>第二层是 <strong>数据链路层</strong>。它定义了数据的传输格式、可以在节点间流动的数据量大小、数据流动可以持续的时长，以及在流中检测到错误时应采取的措施。</p>
<p>使用更加正式的技术术语描述如下：</p>
<ul>
<li><strong>线路规划。</strong> 谁应该交流多久？节点传输信息的时间应该持续多久？</li>
<li><strong>流量控制。</strong> 应该传输的数据量是多少？</li>
<li><strong>错误控制-检测和校正。</strong> 从电尖峰脉冲到卑鄙的连接器，所有的数据传输方法都有可能出错。一旦第二层的技术告知网络管理员有关第二层或第一层的问题，系统管理员就能为后续几层纠正那些错误。第二层主要关心的是错误检测，而不是错误校正。（<a href="https://learning.oreilly.com/videos/wireshark-for-packet/9781839212352/9781839212352-video3_10">来源</a>）</li>
</ul>
<p>第二层内有两个截然不同的子层：</p>
<ul>
<li><strong>介质访问控制（MAC，Media Access Control）：</strong> MAC 子层负责分配硬件标识号，这个标识号被称为 MAC 地址，它能够唯一标识网络上的各个设备。两个设备不应该有相同的 MAC 地址。MAC 地址在硬件制造时就分配好了，位于网卡当中，大多数网络都会自动对其进行识别。交换机会跟踪网络上所有的 MAC 地址。在<a href="https://www.pcmag.com/encyclopedia/term/mac-address">这里</a>和<a href="https://people.richland.edu/dkirby/141macaddress.htm">这里</a>了解更多有关 MAC 地址的信息，在<a href="https://www.networkworld.com/article/3584876/what-is-a-network-switch-and-how-does-it-work.html">这里</a>进一步了解网络交换机。</li>
<li><strong>逻辑链路控制（LLC，Logical Link Control）:</strong> LLC 子层处理帧的寻址以及流量控制。速度取决于两个节点之间的链路，例如以太网或 Wifi。</li>
</ul>
<p>第二层的数据单元是 <em>帧（frame）。</em></p>
<p>每一帧都包括一个帧头、主体和一个帧尾：</p>
<ul>
<li>帧头：通常包括源节点和目的节点的 MAC 地址。</li>
<li>主体：由要传输的比特组成。</li>
<li>帧尾：包括错误检测信息。当检测到错误时，根据实现或网络的配置或协议，帧可能被丢弃，或者错误会被报告给上面的层，用于进一步错误校正。例如，错误检测机制的有循环冗余校验（CRC，Cyclic Redundancy Check）和帧校验序列（FCS，Frame Check Sequence）。<a href="http://www.msc.uky.edu/ken/cs471/notes/chap5.htm">从这里了解更多有关错误检测技术的信息</a>。</li>
</ul>
<figure>
    <img src="https://chinese.freecodecamp.org/news/content/images/2021/11/5-Frame-Example.jpeg" width="1394" height="541" alt="5-Frame-Example" loading="lazy">
    <figcaption><a href="https://learning.oreilly.com/library/view/the-illustrated-network/9780128110287/xhtml/chp001.xhtml">来源</a></figcaption>
</figure>
<p>帧的大小通常有一个最大值，这个值被称为最大传输单元（MTU，Maximum Transmission Unit）。巨型帧的大小超过了标准的 MTU，<a href="https://kb.netgear.com/25091/Guidance-on-the-use-of-jumbo-frames">从这里了解更多有关巨型帧的信息</a>。</p>
<h3 id="osi">如何排查 OSI 第二层中的问题</h3>
<p>这里是第二层中要当心的一些问题：</p>
<ul>
<li>可能在第一层上发生的所有问题</li>
<li>两个节点间的连接（会话）不成功</li>
<li>成功建立但又间歇性失败的会话</li>
<li>帧冲突</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p><em>数据链路层允许局域网内的各节点彼此相互通信。这一层建立了线路规划、流量控制和错误控制的基础。</em></p>
<h2 id="osi">OSI 第三层</h2>
<p>第三层是 <strong>网络层。</strong> 就是在这里，我们通过路由器在网络间或跨网发送信息。不仅仅是节点到节点的通信，我们现在还可以进行网络到网络的通信了。</p>
<p>路由器是第三层的主力——它们是在第三层中必不可少。路由器跨越多个网络移动数据包。</p>
<p>路由器不仅通过连接到网络服务提供商（ISPs，Internet Service Providers）提供因特网访问，还跟踪着所在网络中的一切（记住交换机跟踪的是一个网络中所有的 MAC 地址），它所连接的其它网络，以及在这些网络中路由数据包的不同路径。</p>
<p>路由器将所有的地址和路由信息都保存在路由表中。</p>
<p>这里是一个简单的路由表示例：</p>
<figure>
    <img src="https://chinese.freecodecamp.org/news/content/images/2021/11/6-Routing-Table.png" width="1150" height="378" alt="6-Routing-Table" loading="lazy">
    <figcaption><a href="https://www.geeksforgeeks.org/routing-tables-in-computer-network/">图片来源 + 从这里了解更多有关路由表的信息</a></figcaption>
</figure>
<p>第三层的数据单元是 <em>数据包（data packet）</em>。通常，每个数据包都包含一个帧 <strong>加上</strong> 一个 IP 地址信息的包装。换句话说，帧被第三层的地址信息封装了。</p>
<p>数据包中传输的数据有时也被称为 <em>负载（payload）</em>。每个包都拥有到达目的地所需的一切，但是它能不能成功抵达就是另外一回事儿了。</p>
<p>第三层上的传输是无连接的、尽力而为的——除了将流量发往它应该去的地方，它们不会做任何事。更多与数据传输有关的协议在第四层。</p>
<p>节点一旦连接到因特网，它就会被赋予一个因特网协议（IP，Internet Protocol）地址，它看起来要么像 172.16.254.4（IPv4 地址），要么像 2001:0db8:85a3:0000:0000:8a2e:0370:7334（IPv6 地址）。路由器在它们的路由表中使用 IP 地址。</p>
<p>IP 地址通过地址解析协议（ARP，Address Resolution Protocol）与物理节点的 MAC 地址相关联，ARP 用节点对应的 IP 地址解析 MAC 地址。</p>
<p>ARP 通常被认为是第二层的一部分，但是由于 IP 地址在第三层以下都不存在，所以 ARP 也是第三层的一部分。</p>
<h3 id="">如何排查第三层中的问题</h3>
<p>这里是第三层中要当心的一些问题：</p>
<ul>
<li>所有可能在之前各层中出现的问题 :)</li>
<li>路由器或其它节点故障或无功能</li>
<li>IP 地址配置不正确</li>
</ul>
<p>很多第三层问题的答案都要求使用像 <em>ping</em>、<em>trace</em>、<em>show ip route</em> 或 <em>show ip protocols</em> 这样的命令行工具。在<a href="https://www.pearsonitcertification.com/articles/article.aspx?p=1730891">这里</a>了解更多与有关一至三层问题排查的信息。</p>
<h3 id="tldr">TL;DR</h3>
<p><em>第三层允许节点连接到因特网并跨越不同网络发送数据。</em></p>
<h2 id="osi">OSI 第四层</h2>
<p>第四层是 <strong>传输层</strong>。在这里，我们会深入探讨了两个节点之间连接的具体细节，以及信息是如何在它们之间进行传输的。第四层建立在第二层的功能之上——线路规划、流量控制和错误控制。</p>
<p>这一层也负责数据包的分段，或者说数据包如何被拆分成小片并发往整个网络。</p>
<p>不像上一层，第四层也理解整个消息，而不只是每个独立的数据包的内容。根据对整个消息的理解，第四层不再一次性发送所有数据包，从而管理网络拥塞。</p>
<p>第四层的数据单元有好几个不同的名字，对于 TCP 而言，数据单元是数据包。对于 UDP 而言，包被称为数据报（datagram）。为了简化，我将只使用数据包这个术语。</p>
<p>第四层中最有名的两个协议是传输控制协议（TCP，Transmission Control Protocol）和用户数据报协议（UDP，User Datagram Protocol）。</p>
<p>TCP 是一个面向连接的协议，优先保证的是数据的质量而不是速度。</p>
<p>TCP 显式地与目的节点建立连接，并要求在数据传输时进行源节点与目的节点之间的握手操作。握手能够确认数据已经被接收。如果目的节点没有收到所有的数据，TCP 就会要求进行重传。</p>
<p>TCP 也会确保数据包以正确的顺序交付或者重组。<a href="https://www.cloudflare.com/learning/ddos/glossary/tcp-ip/">从这里了解更多有关 TCP 的信息</a>。</p>
<p>UDP 是一个无连接的协议，优先保证速度而不是数据的质量。UDP 不要求进行握手，这也正是它被称为无连接的原因。</p>
<p>因为 UDP 不必等待确认，所以它可以以更快的速度发送数据，但并非所有的数据都能成功传输，我们也不会知道哪些数据传输失败了。</p>
<p>如果信息被拆分成多个数据报，除非这些数据报都包含一个序列号，否则 UDP 无法确保以正确的顺序重组数据包。<a href="https://www.cloudflare.com/learning/ddos/glossary/user-datagram-protocol-udp/">从这里了解更多有关 UDP 的信息</a>。</p>
<p>TCP 和 UDP 都将数据发往网络设备上的特定端口，这些网络设备都有自己的 IP 地址。IP 地址和端口号的组合被称为套接字（socket）。</p>
<p><a href="https://www.dummies.com/programming/networking/cisco/network-basics-tcpudp-socket-and-port-overview/">从这里了解更多有关套接字的信息</a>。</p>
<p>从<a href="https://www.geeksforgeeks.org/differences-between-tcp-and-udp/">这里</a>和<a href="https://www.pearsonitcertification.com/articles/article.aspx?p=2873377">这里</a>了解 TCP 与 UDP 这两个协议之间的更多差异和相似之处。</p>
<h3 id="osi">如何排查 OSI 第四层中的问题</h3>
<p>这里是第四层中要当心的一些问题：</p>
<ul>
<li>所有可能在之前各层中出现的问题 :)</li>
<li>被封锁的端口——检查你的访问控制列表（ACL，Access Control List）和防火墙</li>
<li>服务质量（QoS，Quality of Service）设置。QoS 是路由器/交换机的一个功能，可以对流量进行优先级排序，并且它们真的可以把事情搞砸。<a href="https://www.pcworld.com/article/2689995/quality-of-service-explained-how-routers-with-strong-qos-make-better-home-networks.html">从这里学习更多有关 Qos 的信息</a>。</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p><em>传输层通过将消息分割成多个数据包提供端到端的消息传输，支持面向连接的和无连接的通信。</em></p>
<h2 id="osi">OSI 第五层</h2>
<p>第五层是 <strong>会话层</strong>，负责建立、维持和终止会话。</p>
<p>会话建立在两个网络应用之间，是双方商定好的连接。注意，我们没有说两个节点，我们已经离开节点了，它们是第四层中的东西。</p>
<p>开玩笑的，我们还是有节点的，但是第五层不需要保留节点的概念，因为它是之前各层抽象出来的（关心）的概念。</p>
<p>所以会话是一个建立在两个特定的用户应用之间的连接，其中有一些重要的概念需要考虑：</p>
<ul>
<li>客户端与服务器模型：请求信息的应用被称为客户端，拥有被请求信息的应用被称为服务器。</li>
<li>请求与响应模型：在建立会话的过程和会话期间，不断有来回的信息请求，还有包含被请求信息的响应或者是“嘿，我没有你要的东西”。</li>
</ul>
<p>会话持续的时间可以非常短，也可以非常长，有时会话也可能会失败。</p>
<p>根据所采用的协议，会话可能会启动各种故障解决程序。根据所使用的应用程序/协议/硬件，会话可能支持单工，半双工或全双工模式。</p>
<p>第五层中协议的例子有网络基本输入输出系统（NetBIOS，Network Basic Input Output System）和远程过程调用协议（RPC，Remote Procedure Call Protocol）等等。</p>
<p>从这里往上（第五层及以上），网络关注的是与用户应用程序建立连接以及如何向用户展示数据。</p>
<h3 id="osi">如何诊断 OSI 第五层中的问题</h3>
<p>这里是第五层中需要当心的一些问题：</p>
<ul>
<li>服务器不可用</li>
<li>服务器未被正确地配置，例如 Apache 或 PHP 配置</li>
<li>会话故障——断连、超时，等等</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p><em>会话层负责初始化、维持并终止两个用户应用程序之间的连接。它响应来自表示层的请求，并向传输层发起请求。</em></p>
<h2 id="osi">OSI 第六层</h2>
<p>第六层是 <strong>表示层</strong>，负责数据的格式，比如字符编码与转换，以及数据加密。</p>
<p>托管用户应用程序的操作系统通常包含第六层中的程序，这个功能并不总是被网络协议实现。</p>
<p>第六层确保第七层中的用户程序可以成功地消费数据，当然还有最终数据的展示。</p>
<p>有三种数据格式化方法需要注意：</p>
<ul>
<li>美国信息交换标准代码（ASCII，American Standard Code for Information Interchange）：这个七位编码技术是字符编码中使用最广泛的标准。ASCII 的一个超集是 ISO-885901，它提供了西欧语言所必需的大多数字符。</li>
<li>扩充的二进制编码的十进制交换码（EBDCIC，Extended Binary-Coded Decimal Interchange Code）：由 IBM 设计，用于大型机。此编码与其他字符编码方法不兼容。</li>
<li>万国码（Unicode）：可以使用 32 位，16 位或 8 位字符的字符编码，它尝试容纳所有已知的字母。</li>
</ul>
<p>从<a href="https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_73/nls/rbagsunicodeandprior.htm">这里</a>、<a href="https://www.smashingmagazine.com/2012/06/all-about-unicode-utf8-character-sets/">这里</a>还有<a href="https://kunststube.net/encoding/">这里</a>了解有关字符编码的更多信息。</p>
<p>加密：SSL 或 TLS 加密协议位于第六层。这些加密协议为网络上的节点提供身份认证和数据加密功能，帮助确保传输的数据抵御恶意用户的攻击。TLS 是 SSL 继任者。</p>
<h3 id="osi">如何诊断 OSI 第六层中的问题</h3>
<p>这里是第六层中需要当心的一些问题：</p>
<ul>
<li>驱动程序不存在或损坏</li>
<li>操作系统用户访问级别不正确</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p><em>表示层负责格式化与加密数据。</em></p>
<h2 id="osi">OSI 第七层</h2>
<p>第七层是 <strong>应用层</strong>。</p>
<p>顾名思义，这一层最终负责支持用户程序使用的服务。应用程序包括安装在操作系统中的软件程序，比如因特网浏览器（例如 Firefox）或文字处理程序（例如 Microsoft Word）。</p>
<p>应用程序可以在后台执行专门的网络功能，也可以要求第七层中专门的服务。</p>
<p>例如专门创建电子邮件程序，它在网络上运行并利用第七层中网络功能（比如电子邮件协议）。</p>
<p>应用程序也可以控制用户交互，比如安全检查（例如 MFA）、识别两名参与者的身份、初始化信息交换等。</p>
<p>这一层中运行的协议包括文件传输协议（FTP，File Transfer Protocol）、安全壳协议（SSH，Secure Shell）、简单邮件传输协议（SMTP，Simple Mail Transfer Protocol）、因特网消息访问协议（IMAP，Internet Message Access Protocol）、域名服务（DNS，Domain Name Service）和超文本传输协议（HTTP，Hypertext Transfer Protocol）。</p>
<p>虽然这些协议中的每一个都服务于不同的功能，运行的方式也各不相同，但从较高的层次看，它们都促进了信息的交流。</p>
<h3 id="osi">如何诊断 OSI 第七层中的协议</h3>
<p>这里是第七层中需要当心的一些问题：</p>
<ul>
<li>所有之前各层中的问题</li>
<li>软件应用程序配置不正确</li>
<li>用户操作失误（我们都遇到过……）</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p><em>应用层拥有用户应用程序运行所需的服务和功能，不包括应用程序本身。</em></p>
<h1 id="">结论</h1>
<p>我们第一层中出现的小考拉已经长大了。</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2021/11/6-Koala-Makeup.jpeg" alt="6-Koala-Makeup" width="1280" height="850" loading="lazy"></p>
<p>学习检查——你能给考拉化妆吗？</p>
<p>没有考拉？</p>
<p>好吧——那就回答这些问题。我保证，这是第二好的选择：</p>
<ul>
<li>什么是 OSI 模型？</li>
<li>每一层分别是什么？</li>
<li>如何使用这些信息去排查网络问题？</li>
</ul>
<p>恭喜！你离理解我们称之为“因特网”的这个宏大实体又进了一步。</p>
<h2 id="">学习资源</h2>
<p>很多非常聪明的人已经写了有关 OSI 模型或特定层的整本书。我鼓励读者阅读 O'Reilly 出版的有关该主题的书，或者网络工程方面的书籍。</p>
<p>这里是我撰写本文时用到的一些资源：</p>
<ul>
<li>The Illustrated Network, 2nd Edition</li>
<li>Protocol Data Unit (PDU):  <a href="https://www.geeksforgeeks.org/difference-between-segments-packets-and-frames/">https://www.geeksforgeeks.org/difference-between-segments-packets-and-frames/</a></li>
<li>Troubleshooting Along the OSI Model:  <a href="https://www.pearsonitcertification.com/articles/article.aspx?p=1730891">https://www.pearsonitcertification.com/articles/article.aspx?p=1730891</a></li>
<li>The OSI Model Demystified:  <a href="https://www.youtube.com/watch?v=HEEnLZV2wGI">https://www.youtube.com/watch?v=HEEnLZV2wGI</a></li>
<li>OSI Model for Dummies:  <a href="https://www.dummies.com/programming/networking/layers-in-the-osi-model-of-a-computer-network/">https://www.dummies.com/programming/networking/layers-in-the-osi-model-of-a-computer-network/</a></li>
</ul>
<h3 id="">关于我</h3>
<p>Chloe Tucker 是位于俄勒冈州波特兰的一名艺术家和计算机科学爱好者。她以前是一名教育工作者，一直在寻找学与教，技术与艺术的交集。你可以通过 Twitter <a href="https://twitter.com/_chloetucker">@_chloetucker</a> 与她联系，还可以访问她的网站 <a href="https://chloe.dev/">chloe.dev</a>。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/osi-model-networking-layers-explained-in-plain-english/">The OSI Model – The 7 Layers of Networking Explained in Plain English</a>，作者：<a href="https://www.freecodecamp.org/news/author/chloe/">Chloe Tucker</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 系统设计面试问题：必备概念 ]]>
                </title>
                <description>
                    <![CDATA[ 你或许听说过“架构”或“系统设计”这两个术语，它们在开发者的求职面试中经常出现，尤其是大型科技公司的招聘人员喜欢提这方面的问题。 这篇教程将深入软件架构的基本概念，帮你做好系统设计面试的准备。 因为系统设计是一个庞大的主题，所以这篇文章并不会面面俱到。但如果你是一名初中级开发者的话，这应该能为你奠定坚实的基础。 你可以从这里深入挖掘其它资源。我已经在文章底部列出了一些我喜爱的资源。 我已经将这篇教程按照主题划分成了很多小模块，我建议你将它加入到书签。我发现 间隔性学习与重复 [https://www.freecodecamp.org/news/use-spaced-repetition-with-anki-to-learn-to-code-faster-7c334d448c3c/]   是获取知识的宝贵工具，真让人难以置信。我已将本教程设计成很多小片段，以便你进行间隔性重复记忆。 第一节：网络与协议（IP、DNS、HTTP、TCP 等） 第二节：存储、延迟与吞吐量 第三节：可用性 第四节：缓存 第五节：代理 第六节：负载均衡 第七节：一致性哈希 第八节：数据库 第九节：领导选 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/systems-design-for-interviews/</link>
                <guid isPermaLink="false">600d32445f61e30501b5c0fe</guid>
                
                    <category>
                        <![CDATA[ 面试 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 系统架构 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Sun, 24 Jan 2021 08:46:44 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/photo-1573497491208-6b1acb260507.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>你或许听说过“架构”或“系统设计”这两个术语，它们在开发者的求职面试中经常出现，尤其是大型科技公司的招聘人员喜欢提这方面的问题。</p>
<p>这篇教程将深入软件架构的基本概念，帮你做好系统设计面试的准备。</p>
<p>因为系统设计是一个庞大的主题，所以这篇文章并不会面面俱到。但如果你是一名初中级开发者的话，这应该能为你奠定坚实的基础。</p>
<p>你可以从这里深入挖掘其它资源。我已经在文章底部列出了一些我喜爱的资源。</p>
<p>我已经将这篇教程按照主题划分成了很多小模块，我建议你将它加入到书签。我发现 <a href="https://www.freecodecamp.org/news/use-spaced-repetition-with-anki-to-learn-to-code-faster-7c334d448c3c/">间隔性学习与重复</a> 是获取知识的宝贵工具，真让人难以置信。我已将本教程设计成很多小片段，以便你进行间隔性重复记忆。</p>
<p><a href="#section1">第一节：网络与协议（IP、DNS、HTTP、TCP 等）</a><br>
<a href="#section2">第二节：存储、延迟与吞吐量</a><br>
<a href="#section3">第三节：可用性</a><br>
<a href="#section4">第四节：缓存</a><br>
<a href="#section5">第五节：代理</a><br>
<a href="#section6">第六节：负载均衡</a><br>
<a href="#section7">第七节：一致性哈希</a><br>
<a href="#section8">第八节：数据库</a><br>
<a href="#section9">第九节：领导选取</a><br>
<a href="#section10">第十节：轮询、流、套接字</a><br>
<a href="#section11">第十一节：端点保护</a><br>
<a href="#section12">第十二节：消息与发布-订阅</a><br>
<a href="#section13">第十三节：必备小知识</a></p>
<p>咱们开始吧！</p>
<h2 id="section1">第一节：网络与协议</h2>
<p>“协议（protocol）”是一个很花哨的词，它在英语中的含义与在计算机科学中的含义截然不同。它表示一套管理事物的规章制度，一种“官方步骤”或“做事情时必须采用的官方做法”。</p>
<p>为了让人们连接到彼此通信的机器和代码，他们需要一个可以在其上进行通信的网络。但是通信也需要一些规则、结构和协商好的步骤。</p>
<p>因此，网络协议（network protocols）是管理给定网络上机器与软件之间如何通信通信的协议。我们热爱的万维网就是网络的一个例子。</p>
<p>你可能听过因特网时代中一些最常见的网络协议，比如 HTTP、TCP/IP 等等。让我们将它们分解成基础构造块吧。</p>
<h3 id=""><a href="https://chinese.freecodecamp.org/news/systems-design-for-interviews/15">因特网协议</a></h3>
<p>把因特网协议（IP，Internet Protocol）视为是协议层的基础吧，它是一个基础协议，指导我们如何实现几乎所有的因特网内通信。</p>
<p>因特网协议上的消息（message）通常以“数据包”的形式进行通信。数据包（packet）是一小团信息（2^16 字节），它的<a href="https://en.wikipedia.org/wiki/IPv4#Packet_structure">核心结构</a>包含两节：协议头（Header）和数据（Data）。</p>
<p>协议头包含与数据包及其内部数据的有关的元数据，比如源 IP 地址（数据包源自哪里）和目的 IP 地址（数据包的目的地）。很明显，这是将信息从一个点发送到另一个点的基础——你需要“从哪来”的地址和“去往哪”的地址。</p>
<p>每个连接到使用 IP 协议通信的 <a href="https://en.wikipedia.org/wiki/Computer_network">计算机网络</a>的设备都会被赋予一个数字标签，这个标签就是 <a href="https://en.wikipedia.org/wiki/IP_address">IP 地址</a>。IP 地址分为公有地址和私有地址，当前有两个版本 IP 协议。新版本被称为 IPv6，它正被逐渐采用，因为 IPv4 地址快耗尽了。</p>
<p>我们在这篇文章中要考虑的其它协议都建立在 IP 之上，就像你最最喜欢的软件语言有建立在它上面的库和框架一样。</p>
<h3 id=""><a href="https://chinese.freecodecamp.org/news/systems-design-for-interviews/19">传输控制协议</a></h3>
<p>传输控制协议（TCP，Transmission Control Protocol）是一个建立在 IP 之上的实用程序。通过阅读我的文章，你可能知道：我坚信你若要真正理解某个东西是做 <em>什么</em> 的，你就要先理解它 <em>为什么</em> 被发明出来。</p>
<p>创建 TCP 是为了解决 IP 的一个问题。通过 IP 传输的数据通常在多个数据包中发送，由于每个数据包都相当小（2^16 字节），所以多个数据包可能出现：(A) 数据包丢失；(B) 乱序。因此导致传输数据损坏。 TCP 通过保证数据包的 <em>有序传输</em> 解决了这些问题。</p>
<p>因为是建立在 IP 之上的，所以 TCP 数据包除了有 IP 头外，还有 TCP 头。这个 TCP 头包含数据包的顺序和数据包的数量等信息。这保证了另一端接收到的数据是可靠的。TCP 通常因建立在 IP 之上而被称为 TCP/IP。</p>
<p>TCP 需要在传输数据包之前建立源端与目的端之间的连接，这是通过“握手”完成的。连接本身是通过使用数据包建立的：源端告知目的端它想打开一个连接，目的端表示同意，然后一个连接就打开了。</p>
<p>这实际上就是服务器“监听”某个端口时发生的事情——在它开始监听前，会先进行一次握手，然后连接被打开（监听开始）。类似地，连接的一端给另一端发送一个打算关闭连接的消息，这就会终止该连接。</p>
<h3 id=""><a href="https://chinese.freecodecamp.org/news/systems-design-for-interviews/20">超文本传输协议</a></h3>
<p>超文本传输协议（HTTP，Hyper Text Transfer Protocol）是一种建立在 TCP/IP 之上的抽象，它引入了一个被称为请求-响应模式非常重要的模式，专门用于客户端-服务端交互。</p>
<p>客户端通常是请求信息的机器或系统，而服务器是以信息进行响应的机器或系统。浏览器是客户端，而 Web 服务器是服务器。当一台服务器向另一台服务器请求数据时，前者是客户端，后者是服务器（赘述，我知道）。</p>
<p>所以这个请求-响应环路在 HTTP 下有自己的规则，这也标准化了因特网间的信息传输方式。</p>
<p>在这层抽象上，我们通常不需要过于担心 IP 和 TCP。然而，HTTP 中的请求与响应不仅有头部，还有主体部分。它们包含了能被开发者设置的数据。</p>
<p>HTTP 请求与响应可以被看作键-值对形式的消息，它们和 JavaScript 中的对象以及 Python 中的字典非常相似，但又有所不同。</p>
<p>下图展示了 HTTP 报文的内容和 HTTP 请求与响应消息中的键-值对。</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/03/image-44.png" width="600" height="400" alt="image-44" loading="lazy">
    <figcaption>来自：<a>https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages</a></figcaption>
</figure>
<p>同 HTTP 一起的还有一些“动词”或“方法”，它们是一些指令，告诉你将要执行何种操作。例如，常见的 HTTP 方法有“GET”、“POST”、“PUT”、“DELETE”和“PATCH”，但远不止这些。找一下上图中起始行中的 HTTP 动词。</p>
<h2 id="section2">第二节：存储、延迟与吞吐量</h2>
<h3 id="">存储</h3>
<p>存储（storage）和保存信息有关。你编写的任何应用、系统或服务都需要保存和检索数据，这正是存储的两个基本功能。</p>
<p>但是存储并不只和保存数据有关，它还涉及到数据的取出。我们使用数据库（database）来实现存储。数据库是一个软件层，它帮助我们保存和检索数据。</p>
<p>保存（storing）和检索（retrieving）这两种主要的操作类型又被称为放置（set）和获取（get）、存储（store）和取出（fetch）、写（write）和读（read），等等。要与存储交互，你需要经过数据库，它是你执行这些基础操作的中间人。</p>
<p>“存储”一词有时能让我们傻傻地从物理的角度思考它。如果我把我的自行车“存储”在仓库，我就能预料到下次打开仓库时它就在那里。</p>
<p>但是，计算机世界中有时并不会这样。存储可以大体分为两种类型：“内存（Memory）”存储和“磁盘（Disk）”存储。</p>
<p>在这两种类型中，磁盘存储往往更稳健，也更具“永久性”（并不是真的永久（permanent），所以我们经常使用“持久化的（persistent）”存储取而代之）。磁盘存储是一种持久化存储，这意味着只要你把东西保存到磁盘，不管断电，还是重启服务器，那些数据都将“持久保存”，不会丢失。</p>
<p>然而，如果你让数据驻留在“内存”中，数据通常会在关机、重启或断电时被擦除。</p>
<p>你每天都在使用的计算机同时拥有这两种类型的存储。你的硬盘是“持久化的”磁盘存储，而你的 RAM 是瞬时的内存存储。</p>
<p>在服务器上，如果你正在记录的数据只在该服务器的会话（session）中有用，那么将它保存在内存中是合理的。这种方式比将东西写入到持久化数据库更快，更实惠。</p>
<p>例如，单个会话可能指用户登录，使用你的网站。在用户退出之后，你可能并不需要紧紧抓住这次会话期间收集到的数据不放。</p>
<p>但是，你会把你任何你想留住的数据（比如购物车历史）放到持久化的磁盘存储中。这样，你可以在用户再次登录时访问那些数据，给用户提供一个无缝的使用体验。</p>
<p>好了，这些似乎非常简单和基础，但实际上并非如此。这是个入门。存储可以变得非常复杂，如果你看一看存储产品和解决方案的规模，很快就会头晕目眩了。</p>
<p>这是因为不同的使用场景需要不同类型的存储。为你的系统选择正确的存储方式的关键取决于很多因素和应用的需求，还有用户如何与之交互。其它的因素包括：</p>
<ul>
<li>数据的形状（结构），或者</li>
<li>数据需要具有的何种可用性（对你的存储来说，什么级别的停机时间是可以出现的），或者</li>
<li>可伸缩性（你需要怎样的数据读写速度，以及这些读写是并发（同时）进行的还是顺序进行的）等等，或者</li>
<li>持久性——如果你使用分布式存储处理停机，那么各存储之间的数据一致性如何？</li>
</ul>
<p>这些问题和结论需要你仔细权衡。一致性的重要程度高于速度吗？你需要数据库抗住每分钟数百万次操作，还是仅用于每晚更新？我将会在后续章节中讨论这些概念，如果你不知道它们是什么，也不用担心。</p>
<h3 id="">延迟</h3>
<p>随着你在支撑前端应用方面的系统设计经验的增加，你会不断听到“延迟”或“吞吐量”这两个术语。总的来说，它们对应用和系统的使用体验和性能至关重要。这些术语的使用普遍都倾向于超出预期范围或脱离上下文，但让我们来解决掉这个问题吧。</p>
<p><strong>延迟（Latency）</strong> 只是对持续时间的度量。什么是持续时间呢？某个动作的持续时间是指完成某事或产生结果所经历的时间。例如：对于从系统中的一个地方移动到另一个地方的数据来说，你可以将持续时间看成延迟（lag），也可以简单地把它看成是完成某个操作所花费的时间。</p>
<p>延迟最常见的理解就是“往返”的网络请求——前端网站（客户端）从给服务器发送查询，到收到服务器返回的响应所花费的时长。</p>
<p>当加载一个网站时，你想要加载过程尽可能的快、整个过程尽可能的流畅。换句话说，你想要的是 <em>低</em> 延迟。快速查找就意味着低延迟。所以在数组中（高延迟，因为你需要迭代数组中的每个元素，找出你想要的那一个）寻找一个值比在哈希表中（低延迟，因为你只需要通过键就可以在“常量”时间内获得数据，不需要进行迭代）更慢。</p>
<p>类似地，从内存读取数据比从磁盘读取数据要快得多（<a href="https://stackoverflow.com/questions/1371400/how-much-faster-is-the-memory-usually-than-the-disk">阅读更多</a>）。但是这两种方式都存在延迟，你的需求会决定为何种数据选择何种类型的存储。</p>
<p>这样看来，延迟是速度的对立面。你想要更高的速度，就是想要更低的延迟。速度（尤其是像通过 HTTP 这样的网络调用）也由距离决定。所以，<a href="https://wondernetwork.com/pings/London">伦敦到另一座城市的延迟</a> 将受该城市与伦敦之间的距离的影响。</p>
<p>想象一下，如果你要设计一个避免 ping 远程服务器的系统，但是为你的系统将数据保存到内存又不太可行。这些都是权衡点，它们让系统设计变得复杂、充满挑战，并且趣味十足！</p>
<p>例如，新闻网站比起加载速度可能更看重正常运行的时间和可用性，而多人游戏可能要求可用性和超低延迟。这些需求将决定基础设施的设计和相关投资，从而支持系统的特殊需求。</p>
<h3 id="">吞吐量</h3>
<p>吞吐量（Throughput）可以被理解为机器或系统的最大容量。它经常在工厂中使用，用于计算装配线一个小时或一天或其它时间计量单位内能完成的工作量。</p>
<p>例如，对一条每小时能够组装二十辆汽车的装配线来说，每小时二十辆汽车就是它的吞吐量。在计算机中，吞吐量指的是单位时间内传送的数据量。所以一个 512 Mbps 的连接就是一种吞吐量的度量方式——512 Mb（兆字节）每秒。</p>
<p>现在想象一下 freeCodeCamp 的服务器。如果它每秒收到一百万个请求，而它只能处理八十万个请求，它的吞吐量就是八十万每秒。你最终可能会用二进制位（bit）的形式而不是请求数量来衡量吞吐量，所以它将会是 N 位每秒。</p>
<p>这个示例中有一个 <em>瓶颈（bottleneck）</em>，因为服务器一秒内最多只能 N 位的数据，但是请求的数据比这个值要高。瓶颈因此成为了系统的约束，系统能达到的最快速度就是它的 <em>最慢瓶颈</em>。</p>
<p>如果一台服务器每秒可以处理一百位，另一台服务器每秒可以处理一百二十位，第三台服务器每秒只能处理五十位，那么整个系统将会以 50bps 的速度进行处理，因为它就是整个系统的约束——它阻挡住了系统中其它服务器的速度。</p>
<p>所以，在瓶颈之外的任何地方增加吞吐量都可能会徒劳无功，你可以先在 <em>最低瓶颈</em> 处增加 <em>吞吐量</em>。</p>
<p>为了增加吞吐量，你可以购买更多的硬件（横向伸缩），也可以增加现有硬件的容量和性能（纵向伸缩），还可以采用另外几种方式。</p>
<p>有时候，增加吞吐量可能只是一个临时的解决方案，所以一个好的系统设计师会全面考虑伸在伸缩给定系统的最佳方式，包括分离请求（或任何其它形式的“负载”），并把他们分发到其它资源上，等等。要牢记的关键点就是：什么是吞吐量，约束或瓶颈又是什么，这些约束和瓶颈是如何影响系统的。</p>
<p>固定延迟和吞吐量并不是独立的、通用的解决方案，它们也不互相关联。它们在整个系统中都有影响和需要考虑的地方，所以了解整个系统以及随时间加入到系统中的需求的本质非常重要。</p>
<h2 id="section3">第三节：系统可用性</h2>
<p>软件工程师的目标是建立可靠系统。可靠系统始终能够满足用户需要，任何时候只要用户有需要，都会被满足。可靠性（reliability）的一个关键部分是可用性（availability）。</p>
<p>将可用性看成系统的弹性（resiliency）是很有帮助的。如果系统足够稳健，能够处理好网络、数据库和服务器等中的故障，就可以被看成是一个容错（fault-tolerant）系统——容错让系统变得可靠。</p>
<p>当然，从多种意义上来说，系统其各个部件的总和，如果可用性关系到终端用户在网站或应用上的使用体验，那么每个部件都需要是 <a href="https://en.wikipedia.org/wiki/High_availability">高可用的</a>。</p>
<h3 id="">量化可用性</h3>
<p>为了量化一个系统的可用性，我们计算给定时间段内系统主要功能和操作处于可用状态下的时长（正常运行时间）所占的百分比。</p>
<p>大多数关键业务系统都需要具有近乎完美的可用性。那些支持具有尖峰和低谷的高可变需求和负载的系统，在非高峰时期的可用性可以稍微低一点。</p>
<p>这一切都取决于系统的使用和性质。但是一般来说，即便是那些有着不变的需求或只“按需”得到保障的系统也需要有高可用性。</p>
<p>想想一个你用来备份照片的网站。你并不总是需要来这个网站检索数据——它主要是用来为你存储东西的。你还是希望每次登录网站时它都是可用的，哪怕只是下载一张照片。</p>
<p>一种不同类型的可用性可以放在像黑色星期五或网络星期一这样的大型电商购物日中进行理解。在这些特定的日子中，需求猛涨，成千上万的用户尝试同时访问订单。这就需要极其可靠和高度可用的系统设计来支撑那些负载了。</p>
<p>高可用性的一个商业原因很简单：网站的任何停机时间都会导致金钱的损失。并且，这会对网站的声誉造成非常恶劣的影响。例如，在一个服务是被 <em>其它</em> 业务用来提供服务的场景中，如果 AWS S3 挂了，那么包括 Netflix 在内的很多公司都会遭殃，这可 <em>不是什么好事儿</em>。</p>
<p>所以上线时间对成功尤为重要。值得牢记的一点是：商业可用性数字是根据年度可用性计算的，所以 0.1% 的停机时间（即 99.9% 的可用性）就是 <a href="https://en.wikipedia.org/wiki/High_availability">一年 8.77 小时</a>。</p>
<p>因此，上线听起来时间极高。看见像 99.99% 上线时间（每年的停机时间仅为 52.6 分钟）的东西是很普遍的，这就是为什么现在普遍使用术语“nines”；来指代正常运行时间——担保正常运行时间中（<a href="https://en.wikipedia.org/wiki/High_availability#%22Nines%22">9 的个数</a>）。</p>
<p>当今世界中，大规模或关键服务宕机是不可接受的。这就是为何现在将“five nines”看成了理想的可用性标准，因为它表示 <em>每年</em> 的停机时间只有五分钟多一点。</p>
<h3 id="">服务级别协议</h3>
<p>为了使得在线服务具有竞争力并满足市场的期望，在线服务提供商通常会提供一个服务级别协议/保证（Service Level Agreements/Assurances）。它们是一系列保证的服务级别指标，99.999% 的正常运行时间就是其中之一，它也经常作为高级订阅服务的一部分。</p>
<p>至于数据库和云服务提供商，如果客户的核心用途可以证明了该指标是符合期望的，它甚至可以在试用或免费套餐中提供。</p>
<p>在很多情况下，如果未能满足 SLA，客户会因提供商未能满足保证而得到信用或其它形式的补偿。<a href="https://cloud.google.com/maps-platform/terms/sla">Google's SLA for the Maps API</a> 就是一个例子。</p>
<p>SLA 因此成为了设计系统时整个商业和技术要考虑的关键部分。考虑可用性是否是系统某个部分的关键需求，以及系统的哪些部分需要具有高可用性尤其重要。</p>
<h3 id="">设计高可用系统</h3>
<p>在设计一个高可用（HA，high availability）系统时，你需要减少或消除“单点故障”。<a href="https://en.wikipedia.org/wiki/Single_point_of_failure">单点故障</a>是系统中的一个部件，它的故障会导致系统丢失可用性。</p>
<p>你可以通过在设计系统时引入“冗余”来去除单点故障。冗余（redundancy）就是为对高可用性有影响的关键元素准备一个或多个替代品（即备份）。</p>
<p>所以，如果你的应用要求用户在使用前必须得到认证，但你又只有一个认证服务和后端，然后它挂了，那么由于它是单点故障，你的系统就没法被使用了。通过准备两个或多个处理认证的服务，你添加了冗余，消除（或减少）了单点故障。</p>
<p>因此，你需要理解并将你的系统拆分为各个部件。找出最有可能导致单点故障的那些部件（那些无法容忍这种错误的部件），以及那些可以容忍错误的部件。因为高可用工程需要进行权衡，而有些权衡可能会在时间、金钱和资源方面非常昂贵。</p>
<h2 id="section4">第四节：缓存</h2>
<p>缓存（caching）是一项用于提升系统性能的技术，非常基础，也很容易理解。它帮助降低系统中的<a href="#section2">“延迟”</a>。</p>
<p>在我们的日常生活中，缓存在大多数时候都是用作一种常识。如果我们住在超市的隔壁，我们还是会想要购买一些基本用品，并将它们保存在冰箱或者柜橱中，这就是缓存。我们总是可以在每次想要食物的时候走出门，来到隔壁，然后购买它们——但是如果储藏室或冰箱中有这些东西的话，我们就减少了做东西吃的时间，那就是缓存。</p>
<h3 id="">常见缓存场景</h3>
<p>类似地，在软件方面，如果我们经常依赖某些数据，我们就会想把那些数据缓存起来，以便应用可以执行得更快。</p>
<p>从内存中检索数据比<a href="#section2">磁盘</a>要快的多，因为网络请求中存在延迟。实际上，很多网站都使用 <a href="https://www.cloudflare.com/learning/cdn/what-is-caching/">CDN</a> 缓存数据（尤其是那些很少改变的内容），以便为最终用户提供更快的服务，降低后端服务器的负载。</p>
<p>缓存的另一个使用场景就是后端需要进行一些计算密集型和耗时的工作。缓存之前的结果可以让你在线性 O(N) 到常量 O(1) 的时间内进行查找，非常有帮助。</p>
<p>同样地，如果你的服务器需要进行多次网络请求和 API 调用才能拿到响应请求所需要的完整数据，缓存数据就能帮你减少网络调用的次数，还有延迟。</p>
<p>如果你的系统有客户端（前端）、服务器和数据库（后端），那么缓存可以被放到客户端上（比如浏览器缓存），也可以放到客户端和服务器之间（比如 CDN），还可以放到服务器本身。这将减少去往数据库的网络调用。</p>
<p>所以缓存可以位于系统中的多个位置或多个级别，包括硬件（CPU）级别。</p>
<h3 id="">处理过期数据</h3>
<p>你可能已经注意到了上面的示例对“读”操作的处理是隐式的。写操作在主要原则上也没什么不同，只是加了以下的考虑：</p>
<ul>
<li>写操作要求缓存与数据库保持同步</li>
<li>这可能会增加复杂度，因为有更多的操作要执行，对未同步或“过期”数据的处理需要仔细分析</li>
<li>新的设计原则可能需要实现对同步的处理——它应该是同步进行，还是异步进行？如果异步进行，那么时间间隔取多大？</li>
<li>数据“驱逐”或更新和数据的刷新，从而保证缓存数据是最新的。这包括像 <a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Last_in_first_out_(LIFO)_or_First_in_last_out_(FILO)">LIFO</a>、<a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#First_in_first_out_(FIFO)">FIFO</a>、<a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)">LRU</a> 和 <a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Least-frequently_used_(LFU)">LFU</a> 这样的技术。</li>
</ul>
<p>所以让我们用一些概括性的、非约束性的结论结束这一节吧。通常来说，在保存静态或不经常改变的数据时，以及变化源很有可能是单个操作而不是用户产生的操作时，缓存表现最好。</p>
<p>在数据的一致性和新鲜度非常关键的场合下，缓存可能就不是最优的解决方案了，除非系统中有一个一个部件在高效地刷新着缓存，并且刷新间隔还不会反过来影响到应用的用户体验。</p>
<h2 id="section5">代理</h2>
<p>代理。啥？我们中的很多人都听说过代理服务器（proxy servers）。我们或许已经在一些自己的 PC 或 Mac 软件上见过关于添加和配置代理服务器的配置项，或者是一些关于如何“通过代理”访问的配置。</p>
<p>所以让我们来看看这个相对简单而又被广泛使用的的重要技术吧。英语中的代理一词与计算机中的代理毫不相关，所以我们先从它的<a href="https://www.merriam-webster.com/dictionary/proxy">定义</a>开始。</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-03-08-at-12.57.03-pm.png" width="600" height="400" alt="Screen-Shot-2020-03-08-at-12.57.03-pm" loading="lazy">
    <figcaption>来自：<a href="https://www.merriam-webster.com/dictionary/proxy">https://www.merriam-webster.com/dictionary/proxy</a></figcaption>
</figure>
<p>现在你可以将大多数含义从脑海中驱逐出去，只留下一个关键词：“代替（substitute）”。</p>
<p>在计算机中，代理通常是一个服务器，并且是一个扮演客户端与另一台服务器之间的中间人角色的服务器。它就是位于客户端与服务器之间的一段代码，这就是代理的关键。</p>
<p>也许你需要复习一下，或者还不太确定客户端和服务器的定义：“客户端”是一个进程（代码）或机器，它从另一个进程或机器（“服务器”）请求数据。当浏览器向一个后端服务器请求数据时，它就是客户端。</p>
<p>服务器为客户端提供服务，但它也可以是客户端——当它从数据库检索数据时，数据库就是服务器，服务器本身既是（数据库的）客户端，<em>又是</em> 前端客户端（浏览器）的服务器。</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/03/image-22.png" width="600" height="400" alt="image-22" loading="lazy">
    <figcaption>来自：<a href="https://teoriadeisegnali.it/appint/html/altro/bgnet/clientserver.html#figure2">https://teoriadeisegnali.it/appint/html/altro/bgnet/clientserver.html#figure2</a></figcaption>
</figure>
<p>如你所见，客户端-服务器关系是双向的。所以一个东西可以同时是客户端和服务器。如果有一个接收请求的中间人服务器，它将请求发给另一个服务，然后转发这个服务给源客户端的响应，那么它就是一个代理服务器。</p>
<p>后面我们将把客户端称作客户端，把服务器称作服务器，把代理称作客户端和服务器之间的东西。</p>
<p>所以当客户端通过代理给服务器发送请求时，代理有时会对服务器掩盖客户端的身份，请求中携带的 IP 地址可能是代理的而不是源客户端的。</p>
<p>对于你们当中那些想要访问网站或下载受限资源（例如从种子网络或被所在国家禁止的网站）的人来说，你们可以识别出这个模式——它就是建立 VPN 的原理。</p>
<p>在我们继续深入之前，我想先说一下：术语代理在使用时通常指的是“转发”代理。转发代理就是在客户与服务器通信过程中代表（或代替）客户端的代理。</p>
<p>这与反向代理不同，反向代理代表的是服务器。图上它看起来和转发代理一样是位于客户端和服务器之间的代理，数据流与客户端&lt;-&gt;代理&lt;-&gt;服务器一样。</p>
<p>二者关键的区别是：反向代理是设计用来代替服务器的。通常客户端根本不知道网络请求是通过代理路由并且是代理将其传递给内部服务器的（对服务器的响应也是这么做的）。</p>
<p>所以，在转发代理中，服务器将不会知道客户端的请求和响应经过了代理。而在反向代理中，客户端经不会知道请求和响应都是经过代理路由的。</p>
<p>代理有点偷偷摸摸的 :)</p>
<p>但是在系统设计中，尤其是对复杂系统来说，代理很有用，反向代理特别有用。你的反向代理可以委派大量你不希望在主服务器处理的任务，它可以是一个网关、一个筛选器、一个负载均衡器和全能助手。</p>
<p>所以代理非常有用，但你可能并不知道为何如此。再一次，如果你读过我的其它材料，你就会知道我坚信：你只有在知道某些东西 <em>为何</em> 存在之后，才能对它们有正确的认识——只知道它们做 <em>什么</em> 并不够。</p>
<h2 id="section6">第六节：负载均衡</h2>
<p>如果你考虑一下负载（load）和均衡（balance）这两个词，你就会开始对它们在计算机世界中所做的事情有了一个直觉。当服务器同时收到大量请求时，它可能会慢下来（吞吐量下降、延迟上升）。达到一定程度之后，服务器甚至会宕机（丧失可用性）。</p>
<p>你可以赋予服务器更多的能力（纵向伸缩），也可以添加更多的服务器（横向伸缩）。但是现在你要解决的是如何将进来的请求分布到不同的服务器上去——哪些请求被路由到哪些服务器以及如何保证这些服务器也不会过载？换句话说，你如何均衡并分配请求负载？</p>
<p>我们现在进入负载均衡器（load balancer）部分。由于这篇文章是一篇对原则和概念的介绍，所以不可避免地，它们都是非常简单的解释。负载均衡器的任务是位于客户端和服务器之间（但是它们也可以被放在其它的地方）并负责将传入请求分发到多个服务器上去，从而不断给终端用户（客户端）带来快速、流畅和可靠的体验。</p>
<p>所以负载均衡器就像指挥交通的交管人员一样。它们这样做是为了维持<a href="#section2">可用性</a>和<a href="#section3">吞吐量</a>。</p>
<p>在理解负载均衡器被放置到系统架构中的哪个位置时，你可以看到负载均衡器可以被看作 <a href="#section5">反向代理</a>。但是负载均衡器也可以被插入到其它地方——其它有着信息交换的部件之间，例如你的服务器和数据库之间。</p>
<h3 id="">均衡操作——服务端选择策略</h3>
<p>所以负载均衡器是如何决定如何路由和分配请求流量的呢？首先，你每新增一台服务器，就需要让负载均衡器知道又多了一台可以将流量路由过去的候选服务器。</p>
<p>如果你去掉了一台服务器，负载均衡器也需要知道。配置信息确保负载均衡器知道它的访问列表中有多少台服务器以及哪些服务器是可用的。持续通知负载均衡器每台服务器的负载级别、状态、可用性、当前任务等信息也是可能的。</p>
<p>将负载均衡器配置为知道可以重定向到哪些服务器后，我们需要制定最佳路由策略，以确保请求被适当地分配到可用服务器之间。</p>
<p>一种简单的方法就是：负载均衡器随机选择一个服务器，然后将每个传入请求转发过去。但是可想而知，随机化会导致负载的“不均衡”分配，一些服务器获得的负载比其它服务器多，这可能会给整个系统的性能带来负面影响。</p>
<h3 id="">轮询和加权轮询</h3>
<p>另一种可以被直观理解的方法叫做“轮询（round robin）”。这是很多人处理循环处理列表的方式。你从列表中的第一个元素开始，依次向下移动，在抵达最后一个元素之后，回到顶点，再次向下处理这个列表。</p>
<p>负载均衡器也可以这么做，它只需要以固定的顺序循环迭代所有的可用服务器。这种负载方式以一种易于理解和可预测的模式完成了多个服务器之间负载的均匀分发。</p>
<p>通过给一些服务“加权”，你可以轮询会变得更加“花哨”。在标准的轮询中，每台服务器都被赋予了相等的权重（所有的服务器的权重都为 1）。但是当你对服务器分开加权后，就能让一些服务器有着更低的权重（比如 0.5，如果它们更弱的话），而另一些服务器会有更高的权重，比如 0.7 或 0.9 甚至 1。</p>
<p>然后整个流量将会根据权重的比例被划分并根据服务器对大量请求的处理能力呈比例分配。</p>
<h3 id="">基于负载的服务器选择</h3>
<p>更加复杂的负载均衡器可以根据当前的容量、性能和列表中的服务器负载进行决策，根据当前负载并计算出如何才能达到最高吞吐量、最低延迟等，然后动态分配负载。负载均衡器监控每台服务器的性能，据此决定哪些服务器不能处理新的请求。</p>
<h3 id="ip">基于 IP 哈希的选择</h3>
<p>你可以配置负载均衡器对传入请求的 IP 地址进行<a href="https://www.cs.cmu.edu/~adamchik/15-121/lectures/Hashing/hashing.html">哈希</a>，然后使用哈希值决定将请求转发到哪台服务器。如果我有五台可用的服务器，那么哈希函数就会被设计返回五个哈希值中的一个，所以肯定会有一台服务器被指派去处理这个请求。</p>
<p>如果你想让来自某个国家或地区的请求从该地区中最能满足需求的服务器获取数据，或者你的服务器通过缓存请求来实现更快的处理，基于 IP 哈希的路由策略会非常有用。</p>
<p>在后一种场景中，你可能想确保请求去往那台之前已经缓存相同请求的服务器，因为这会提高处理和响应请求的速度与性能。</p>
<p>如果你的每台服务器都维护着独立的缓存，负载均衡器也不总是将相同的请求转发到同一台服务器，结果可能就是服务器重复做着之前已经做过的工作，因为之前的请求去了另一台服务器，而你又不能利用那些缓存数据进行优化。</p>
<h3 id="">基于路径或服务的选择</h3>
<p>你也可以让负载均衡器根据请求的“路径”或功能或被提供的服务路由请求。例如，如果你正从一家网上花店买花，加载“Bouquets on Special”的请求可能被发送到一台服务器，而信用卡支付的请求可能被发送到另一台服务器。</p>
<p>如果二十位访问者中实际上只有一位买了花，那么你可以让一个较小的服务器处理支付，让一个更大的服务器处理所有的浏览流量。</p>
<h3 id="">混合包</h3>
<p>和其它东西一样，你可以达到更高和更细层次的复杂度。你可以有多个负载均衡器，它们各自有着不同的服务器选择策略！此外，如果你的系统是一个非常大的高流量系统，你或许需要 <em>负载均衡器的负载均衡器……</em></p>
<p>最终，你需要不断地往系统中添加组件，直到系统的性能适应了你的需求（你的需求可能看起来平缓，或者随时间缓慢上升，或者容易出现峰值）。</p>
<p>我们已经讲了 VPN（用于转发代理）和负载均衡（用于反向代理），但是 <a href="https://www.quora.com/What-are-the-different-uses-of-proxy-servers">这里</a> 还有更多的例子。</p>
<h2 id="section7">第七节：一致性哈希</h2>
<p>负载均衡中的哈希是我们要理解的稍微有点棘手的概念之一，因此它有了自己单独的一节。</p>
<p>为了理解它，请首先了解 <a href="https://www.cs.cmu.edu/~adamchik/15-121/lectures/Hashing/hashing.html">哈希在概念上是如何工作的</a>。简单来说，哈希将输入转换成一个固定长度的值，通常是一个整数（哈希值）。</p>
<p>对于一个好的哈希算法或函数而言，一个关键的原则是函数本身必须是<a href="https://en.wikipedia.org/wiki/Hash_function#Deterministic">可确定的</a>，也就是相同的输入在传入函数之后会得到相同的输出。所以，可确定性（determinstic）表示：如果我传入了一个字符串“Code”（大小写敏感）然后函数生成的哈希值为 11002，那么每次我传入“Code”时，它就必须生成整数“11002”。如果我传入“code”，它就会生成另一个不同的数字（这个数字也是不变的）。</p>
<p>有时，哈希函数可能会为多个输入生成相同的哈希值——不要担心，我们还有应对方式。实际上，唯一输入的范围越大，出现这种情况的可能性就越大。但是当不只一个输入生成相同的输出时，就产生了“冲突（collision）”。</p>
<p>牢记这一点，让我们把它用到路由和将请求转发到服务器的过程中。假设你有五台用于分配负载的服务器，一种容易理解的方法就是对传入请求（可能是 IP 地址，或者一些其它的客户端信息）进行哈希，为每个请求生成哈希值。然后你就可以对这个哈希值进行取模运算，运算的右操作数为服务器的数量。</p>
<p>例如，你的负载均衡器的伪代码可能长这样：</p>
<pre><code class="language-javascript">request#1 =&gt; hashes to 34
request#2 =&gt; hashes to 23
request#3 =&gt; hashes to 30
request#4 =&gt; hashes to 14

// 你有五台服务器 =&gt; [Server A, Server B, Server C, Server D, Server E]
// 所以对每个请求的哈希值进行模 5 运算……
request#1 =&gt; hashes to 34 =&gt; 34 % 5 = 4 =&gt; send this request to servers[4] =&gt; Server E
request#2 =&gt; hashes to 23 =&gt; 23 % 5 = 3 =&gt; send this request to servers[3] =&gt; Server D
request#3 =&gt; hashes to 30 =&gt; 30 % 5 = 0 =&gt; send this request to  servers[0] =&gt; Server A

</code></pre>
<p>如你所见，哈希函数生成了大量可能的值，取模运算符把这个值的范围变小了，刚好映射到了服务器的数量。</p>
<p>你肯定会遇到不同的请求被映射到同一台服务器，这还好，只要在所有服务器上的所有分配是“<a href="https://en.wikipedia.org/wiki/Hash_function#Uniformity">一致的</a>”就行。</p>
<h3 id="">添加服务器，处理故障服务器</h3>
<p>所以，如果我们向其发送流量的服务器挂了会怎样呢？哈希函数（我们上面的那一段伪代码）仍然认为还有五台服务器，取模运算符会生成 0-4 这个范围的数字。但是由于一台服务器挂了，我们只有四台服务器了，可我们还是向那台挂了的服务器发送流量。糟糕。</p>
<p>相反，我们可以加入第六台服务器，但它 <em>永远</em> 都不会收到任何流量，因为我们的模操作数为 5，它永远也不会产生一个将第六台服务器包括进来的数字。更糟糕了。</p>
<pre><code>// 添加第六台服务器
servers =&gt; [Server A, Server B ,Server C ,Server D ,Server E, Server F]
// 将模操作数改成 6
request#1 =&gt; hashes to 34 =&gt; 34 % 6 = 4 =&gt; send this request to servers[4] =&gt; Server E
request#2 =&gt; hashes to 23 =&gt; 23 % 6 = 5 =&gt; send this request to servers[5] =&gt; Server F
request#3 =&gt; hashes to 30 =&gt; 30 % 6 = 0 =&gt; send this request to  servers[0] =&gt; Server A

</code></pre>
<p>我们发现服务器数字在取模后变了（尽管，在这个示例中，request#1 和 request#3 <em>没有改变</em>——但这只是一个特例）。</p>
<p>于是，现在总共有一半的请求（其它示例中可能会更多！）被路由到了新的服务器，我们因此失去了之前缓存在服务器上的数据的所带来的好处。</p>
<p>例如，request#4 之前去往 Server E，但是现在去了 Server C。Server E 中所有与 request#4 有关的缓存数据都没用了，因为现在请求都去了 Server C。你可以为一台宕机的服务器计算一个类似的问题，但是取模函数仍然给它发送请求。</p>
<p>在这个微型系统中，它看似没啥影响。但在一个超大规模的系统中，这就是一个糟糕的结果。系统设计失败。</p>
<p>显然，一个简单地通过哈希分配请求的系统不能很好地处理错误。</p>
<h3 id="">流行的解决方案——一致性哈希</h3>
<p>不幸的是，我觉得用文字并不足以描述这一部分。一致性哈希（consistent hashing）最好是通过可视化进行理解。但是到目前为止，本篇文章的目的是给你关于该问题的直观想法，为什么会出现这个问题，以及常规解决方案可能的不足。牢记这一点。</p>
<p>正如我们讨论的，普通哈希的关键问题在于：当 (A) 一个服务器宕机了，仍然有流量被路由到它，并且 (B) 你添加了一台新的服务器，分配方式本质上已经变了，因此你失去了之前的缓存带来的好处。</p>
<p>在深入一致性哈希时，有两个重要的点需要牢记：</p>
<ol>
<li>一致性哈希 <em>没有消除问题</em>，尤其是 B。但是它确实减少了很多问题。一开始你可能想知道为什么一致性哈希那么重要，底层的缺点依然存在，确实如此，但是它们变少了，并且一致性哈希本身也是对大规模系统的一个有效改进。</li>
<li>一致性哈希对传入请求 <em>和服务器</em> 进行哈希操作，哈希结果因此陷入一系列（连续）的值。这个细节非常重要。</li>
</ol>
<p>请在观看下面推荐的解释一致性哈希的视频时牢记这一点，否则它的效果就没那么明显了。</p>
<p>我强烈推荐这个视频，它融入了这些原则，却没有过多烦扰你的细节。</p>
<figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe width="560" height="315" src="https://www.youtube.com/embed/tHEyzVbl4bg" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" name="fitvid0"></iframe>
          </div>
        </div>
      </figure>
<center>一个关于一致性哈希的简单介绍，来自 Hannah Barton</center>
<p>如果你在理解消化为什么这个策略在负载均衡中很重要时遇到了任何问题，我建议你先休息一下，然后回到 <a href="#section6">负载均衡一节</a>，再次阅读。除非你在工作中遇到过这个问题，否则所有的这些都会变得非常抽象，这并不罕见。</p>
<h2 id="section8">第八节：数据库</h2>
<p>我们简要地<a href="#section2">考虑了</a>为满足许多不同使用场景设计的不同类型的存储方案（数据库），并且其中的一些和其它的相比，更适用于特定的任务。站在一个非常高的视角，数据库可以被分类成两种类型：关系型（Relational）和非关系型（Non-Relational）。</p>
<h3 id="">关系型数据库</h3>
<p><strong><a href="https://en.wikipedia.org/wiki/Relational_database">关系型数据库</a></strong> 是一种严格执行数据库中所存事物之间关系的数据库。这些关系通常可能会要求数据库将每个东西（称为“实体”）表示成一个结构化表，表中有零行或多行（“记录”，“条目”）和一列或多列（“属性”，“字段”）。</p>
<p>通过在实体上强制推行这种结构，我们可以确保每个数据项/条目/记录都具有正确的数据。它带来了更好的一致性，以及形成实体间更精密关系的能力。</p>
<p>你可以从下面记录“Baby”（实体）数据的表中看到这个结构。表中的每条记录（“条目”）都有四个字段，它们表示与那个宝宝有关的数据。这是一个经典的关系型数据库结构（也是一个被称为<a href="https://en.wikipedia.org/wiki/Database_schema">模式</a>的规范实体结构）。</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/03/image-46.png" width="600" height="400" alt="image-46" loading="lazy">
    <figcaption style="text-align: center">
        来自：<a href="https://web.stanford.edu/class/cs101/table-1-data.html">https://web.stanford.edu/class/cs101/table-1-data.html</a>
    </figcaption>

<p>所以理解关系型数据库的关键在于它们是高度结构化的，并且将结构强加给所有的实体。通过确保添加到表的数据符合该结构来强制实施此结构。添加一个模式不允许的高度字段到表将不被准许。</p>
<p>大多数关系型数据库都支持数据库查询语言，叫做 SQL——<a href="https://en.wikipedia.org/wiki/SQL">结构化查询语言</a>。这是一门专为与结构化（关系型）数据库内容交互设计的语言。这两个概念的联系非常紧密，以至于很多人通常将关系型数据库称作“SQL 数据库”（有时发音为“sequel”数据库）。</p>
<p>通常认为，SQL（关系型）数据库比非关系型数据库支持更加复杂的查询（组合不同的字段、过滤器和条件）。数据库本身处理这些查询，然后返回满足的结果。</p>
<p>很多 SQL 数据库迷都说，如果没有这个功能（指复杂查询）的话，你将不得不先取出 <em>所有的</em> 数据，让服务器或客户端把数据加载进 “<a href="#section2">内存</a>”，然后应用过滤条件——这在小数据集上还好，但是对于有着成千上万的记录和行的大型复杂数据集来说，就会对性能带来非常严重的影响。然而，并不总是这样，我们将会在学习 NoSQL 数据库时看到原因。</p>
<p>一个常见而又倍受喜爱的关系型数据库是 <a href="https://en.wikipedia.org/wiki/PostgreSQL">PostgreSQL</a>（通常称为“Postgres”）数据库。</p>
<h3 id="acid">ACID</h3>
<p>ACID 事务描述的是良好关系型数据库都将支持的事务的一组特征。<a href="https://en.wikipedia.org/wiki/ACID">ACID = "Atomic, Consistent, Isolation, Durable"</a>。事务是与数据库的交互，通常是读或写操作。</p>
<p><strong>原子性（Atomicity）</strong> 要求：当单个事务包含到超过一个操作时，数据库必须保证如果有一个操作失败，<em>整个</em> 事务（所有的操作）也会失败。它就是“要么全部，要么一个也不”。这样，如果事务成功了，那么你会知道所有的子操作都成功了，如果某个操作失败了，那么你会知道所有同它一起的操作都失败了。</p>
<p>例如，如果一个事务包括从两个表中读数据并往三个表写数据，那么如果它们中的任意一个操作失败了，整个事务就失败了。这意味着一个独立的操作都不应该完成。你甚至不希望三个写操作中的一个成功——这会“弄脏”数据库内的数据！</p>
<p><strong>一致性（Consistency）</strong> 要求：数据中的每个事务都符合数据库定义的规则，当数据库改变状态（一些信息改变了）时，这个改变是有效的，不会损坏数据。每个事务都将数据库从一个 <em>有效的</em> 状态移动到另一个 <em>有效的</em> 状态。可以这么看待一致性：每个“读”操作都收到最近一次“写”操作的结果。</p>
<p><strong>隔离性（Isolation）</strong> 表示你可以在数据库“并发（concurrently）”（同时）运行多个事务，但是数据库最终的状态看起来就像每个操作串行（按顺序，就像一个操作队列一样）运行结束后一样。我个人认为“隔离性”对概念的描述性不是非常强，但是我猜 ACCD 比 ACID 说起来更难……</p>
<p><strong>持久性（Durability）</strong> 保证：一旦数据被保存到数据库中，就会一直在那里。它将会是“<a href="#section2">持久的</a>”——保存在硬盘，而不是“内存”中。</p>
<h3 id="">非关系型数据库</h3>
<p>相反，<strong>非关系型数据库</strong> 就没有那么死板了，或者，换句话说，它的数据有着更加灵活的结构。数据通常以“键-值”对的形式呈现。这种方式的一个简化形式就是一个由“键-值”对对象构成的数组（列表），例如：</p>
<pre><code class="language-javascript">// baby names
[
	{ 
    	name: "Jacob",
        rank: ##,
        gender: "M",
        year: ####
    },
    { 
    	name: "Isabella",
        rank: ##,
        gender: "F",
        year: ####
    },
    {
      //...
    },
    
    // ...
]
</code></pre>
<p>非关系型数据库也被称为“NoSQL”数据库，当你不想或不需要结构一致的数据时，它能给你提供便利。</p>
<p>和 ACID 性质类似，NoSQL 数据库的性质有时候被称为 BASE：</p>
<p>**基本可用（Basically Abailable）**规定系统保证可用性</p>
<p><strong>软状态（Soft State）</strong> 表示系统的状态可能随时间改变，即使没有输入也是如此</p>
<p><strong>最终一致性（Eventual Consistency）</strong> 规定系统在一段（短暂）的时间间隔之后会达到一致的状态，除非收到了其它的输入。</p>
<p>由于这些数据库的内核都采用类哈希表结构保存数据，所以它们会非常快、简单、易用，完美适用于缓存、环境变量、配置文件和会话状态等使用场景。这种灵活性使得它们在内存中（比如 <a href="https://en.wikipedia.org/wiki/Memcached">Memcached</a>）和持久化存储（比如 <a href="https://en.wikipedia.org/wiki/Amazon_DynamoDB">DynamoDb</a>）中的使用非常完美。</p>
<p>还有很多“类 JSON” 的数据库，它们被称为文档数据库，比如倍受喜爱的 <a href="https://www.mongodb.com/document-databases">MongoDb</a>，这些数据库的内核也采用“键-值”存储。</p>
<h3 id="">数据库索引</h3>
<p>这是一个复杂的话题，所以为了给你一个关于系统设计面试需要哪些知识的高度概述，我将会简单地给出冰山一角。</p>
<p>想象一个有一亿行数据的数据库表，这个表主要被用来查找每条记录的一个或者两个值。为了得到特定行的值，你需要迭代整个表，如果它是非常靠后的记录，那将会花费很长时间！</p>
<p>索引（indexing）是记录的一种快捷方式，它在匹配值时比逐行检查的更加高效。索引通常是一种被添加到数据库的数据结构，它专为促进数据库内 <em>特定</em> 属性（字段）上的快速搜索而设计。</p>
<p>所以，如果人口统计局有一亿两千万条具有名字和年龄的记录，而你最常需要的就是检索属于某个年龄组的人员列表，那么就可以在该数据库的年龄属性上建立索引。</p>
<p>索引是关系型数据库的核心，在非关系型数据库中也广泛使用。因此，从理论上讲，索引的好处可用于两种类型的数据库，这对优化查找时间非常有利。</p>
<h3 id="">复制和分片</h3>
<p>虽然这些听起来像是一部生物恐怖电影中的东西，但你更有可能每天都在数据库扩展中听到它们。</p>
<p>复制（replication）是指复制（duplicate, make copies of, replicate）你的数据库。你或许记得我们在<a href="#section3">可用性</a>一节讨论过它。</p>
<p>我们已经考虑了在系统内进行冗余以维持高可用带来的好处。复制在单个数据库宕机时保证数据库的冗余。但是它也引发了如何在副本中同步数据的问题，因为它们要有相同的数据。数据库写操作和更新操作的复制可以同步进行（主数据库发生改变的同时），也可以异步进行。</p>
<p>主库与从库之间同步数据的可接受时间间隔取决于具体的需求——如果你确实需要两个数据库之间的状态是一致的，那么复制操作必须非常快。你也想确保从库上的写操作失败时，主库上的写操作也失败（原子性）。</p>
<p>但是，当你拥有如此多的数据，以至于简单地进行复制可能会解决可用性问题，但不能解决吞吐量和延迟问题（速度）时，你会怎么做？</p>
<p>这个时候你可能会考虑对你的数据进行“分类”，形成“分片（shard）”。一些人也管这个叫数据划分（partitioning），它与硬盘分区完全不同。</p>
<p>数据分片将大数据库拆分为较小的数据库。你可以根据数据结构决定如何对数据进行分片。分片可以像每五百万行一个分片这么简单，也可以采用最适用于当前数据、需要和服务位置的其它策略。</p>
<h2 id="section9">第九节：领导选举</h2>
<p>让我们再次回到服务器，讨论一个稍微高级点的话题。我们已经了解了 <a href="#section3">可用性</a>的原则，以及冗余是如何作为增加可用性的一种手段的。在处理到冗余服务器集群的<a href="#section6">请求路由</a>时，我们还介绍了一些实践中的注意事项。</p>
<p>但是有时候，在这种设置中，多个服务器中的事情没多大差别，这可能会出现只需要一台服务器带头的情况。</p>
<p>例如，你想确保只有一台服务器负责更新某些第三方 API，因为来自不同服务器的多次更新可能导致问题，或者增加第三方的成本。</p>
<p>在这种情况下，你需要选择一台主服务器，然后将更新职责委派给它。这个过程被称为 <a href="https://en.wikipedia.org/wiki/Leader_election">领导选举</a>。</p>
<p>当一个集群中有多台服务器提供冗余时，可以配置它们之间有且只有一个领导。它们也会在检测到领导服务器宕机时，指定另一台服务器顶替领导服务器的位置。</p>
<p>原理非常简单，但细节才是最让人头疼的。真正棘手的部分在于：确保服务器之间保持数据、状态和操作的同步。</p>
<p>例如，始终存在某些事故导致一台或两台服务器断开与其他服务器之间的连接的风险。在这种情况下，工程师们最终会使用一些区块链中的基本理念让服务器集群达成共识。</p>
<p>换句话说，<a href="https://en.wikipedia.org/wiki/Consensus_algorithm">共识算法</a> 用于告知所有服务器一个“达成一致的”值，它们都可以在识别领导服务器的逻辑中依赖这个值。</p>
<p>领导选举通常使用像 <a href="https://etcd.io/">etcd</a> 这样的软件实现，它是一个键-值对存储，通过使用领导选举本身和共识算法，它同时提供了可用性 <em>和</em> 强一致性（这很有价值，也是不寻常的组合）。</p>
<p>所以工程师们可以依赖 etcd 自己的领导选举架构，在他们的系统中进行领导选举。这是通过在像 etcd 这样的服务中存储一个表示当前领导的键-指对来完成的。</p>
<p>因为 etcd 是高可用 <em>和</em> 强一致性的，所以你总是可以在自己的系统中依赖那个键-值对，它包含集群中关于当前领导是哪台服务器的最终“事实来源”。</p>
<h2 id="section10">第十节：轮询、流、套接字</h2>
<p>在这个不断更新、推送通知、流内容和实时数据的现代时代，掌握支撑这些技术的基本原理非常重要。要定期或立即更新应用中的数据，你需要使用以下两种方法中的一种。</p>
<h3 id="">轮询</h3>
<p>这种方法很简单。如果查看 <a href="https://en.wikipedia.org/wiki/Polling_(computer_science)">维基百科词条</a>，你会发现它讲的有点多。所以还是看一看词典中是如何解释它吧，尤其是计算机科学背景下的含义。牢记这个简单的基础。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-2020-03-14-at-10.25.44-am.png" alt="Screen-Shot-2020-03-14-at-10.25.44-am" width="600" height="400" loading="lazy"></p>
<p>轮询（polling）就是简单地让客户端检查服务器，客户端发送一个网络请求请求更新的数据。这些请求通常按照固定的时间间隔发出，比如五秒、十五秒、一分钟或用例要求的任何时间间隔。</p>
<p>每几秒钟进行一次轮询还是和实时大有不同，还以下缺点，特别是你的并发用户在一百万以上的时候：</p>
<ul>
<li>几乎不变的网络请求（不利于客户端）</li>
<li>几乎不变的入站请求（不利于服务端负载——每秒超过一百万次请求！）</li>
</ul>
<p>所以快速轮询并不是真的很高效，轮询最好是用在数据更新中的小差别对应用来说不是问题的场景中。</p>
<p>例如，如果你克隆了 Uber，你或许会让司机端应用每五秒发送一次司机的位置数据，让乘客端应用每五秒轮询一次司机的位置。</p>
<h3 id="">流</h3>
<p>流（streaming）解决了不断轮询的问题。如果有必要不断访问服务器，最好使用像 <a href="https://en.wikipedia.org/wiki/WebSocket">web-socket</a> 这样的东西。</p>
<p>这是一个旨在 TCP 上工作的网络通信协议。它在客户端和服务器之间打开一个双向的专用通道（套接字），有点像在两个端点之间的开放热线。</p>
<p>与通常的 TCP/IP 通信不同，这些套接字是“长久的（long-lived）”，以便它向服务器发出的单个请求打开了进行双向数据传输的热线，而不是采用多个独立的请求。我们使用长久一词表示机器之间的套接字连接将会一致持续到连接的一端关闭它，或者网络故障。</p>
<p>你可能还记得我们对 IP、TCP 和 HTTP 的讨论，这些操作在每个请求-响应环中都发送数据的“数据包”。Web-socket 意味着只有一个请求-响应交互（不是你认为的环！），它打开了一个通道，通道两端数据都以“流”的形式发送。</p>
<p>轮询与所有“常规”的基于 IP 的通信的一大区别就是：轮询让客户端定时给服务器发送获取数据的请求（“拉取”数据），而在流中，客户端“随时等待”着服务器将一些数据“推送”给它。服务器会在数据改变时将其发出，并且客户端总是在监听它。因此，如果数据不断变化，那么它就成为了一个“流”，这可能更加符合用户的需求。</p>
<p>例如，在使用 <a href="https://www.freecodecamp.org/news/p/51a1d601-c57e-48cf-8f8d-9bb1c333d64d/repl.it">协作式编码 IDE</a> 时，只要有用户输入了东西，它就可以立马被其它人看到，这就是通过 web-socket 实现的，因为你想要的是实时协作。如果我输入的内容在你输入同样的内容之后才在你的屏幕上显示，或者你等了三分钟才知道我干了啥，那就太糟糕了！</p>
<p>或者想一下多人在线游戏——那是在玩家之间的流式传输游戏数据的绝佳案例！</p>
<p>总之，使用场景决定选择轮询还是流。通常，如果你希望数据是“实时的”，就用流；如果滞后（十五秒也是滞后）对你来说是可以的，那么轮询也是一个不错的选择。但是这完全取决于你有多少并发用户，以及他们是否期望数据是实时的。流式服务的一个常见示例就是 <a href="https://en.wikipedia.org/wiki/Apache_Kafka">Apache Kafka</a>。</p>
<h2 id="section11">第十一节：端点保护</h2>
<p>当你构建大规模系统时，保护系统免受过多操作（使用系统时实际上不需要这些操作）就变得很重要了。现在听起来还是非常抽象。但是想一下这个——你有多少次疯狂点击着一个按钮，以为它将会使系统更快？想象一下如果每次点击都 ping 一次服务器，服务器也尝试全部处理它们！如果系统的吞吐量由于某些原因很低（比如服务器正在异常负载下挣扎），那么每次点击都会使系统变得更慢，因为系统必须处理所有的点击！</p>
<p>有时它甚至与保护系统无关。有时你想要限制操作次数，因为那是你提供服务的一部分。例如，你使用了第三方 API 服务的免费套餐，他们只允许你每三十分钟进行二十次请求。如果你在三个分钟内进行了二十一次或三百次请求，那么在前二十次请求后，服务器将不再处理你的请求。</p>
<p>这被称为限流（rate-limiting）。服务器可以通过限流手段限制客户端给定时间段内尝试进行的操作数量。限流可以根据用户计算，也可以根据请求、次数、负载或任何其它东西。通常，一旦某个时间段内的操作超出了限制，服务器会在剩余的时间内返回错误。</p>
<p>好了，现在你可能认为端点“保护”有些夸张。你只是在限制用户从端点获取某些东西的能力。的确如此，但是它面对恶意用户（客户端）时也能提供保护——就像一个机器人正在攻击你的端点一样。为什么会发生那种事情呢？因为用超过服务器处理能力的大量请求淹没服务器是一种策略，这是恶意攻击者让服务器宕机的一种方式，这种方式也能有效击垮服务。它正是 <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">拒绝服务（DoS，Denial of Service）攻击</a>。</p>
<p>虽然限流可以抵御 DoS 攻击，但是它本身并不能把你从高级版本的 DoS 攻击——<em>分布式</em> DoS 中解救出来。这里的分布式只是简单地表示攻击来自多个看似毫不相关的客户端，也没有识别它们是被单个恶意代理控制的有效方式。需要采取其它方式来防止这种协作型分布式攻击。</p>
<p>但是无论如何，对于像我在上面提到的 API 限制这些没那么恐怖的场景而言，限流是一种有用且流行的方式。考虑到限流的工作原理，由于服务器必须先检查限制条件并在必要时强制实行，所以你需要考虑使用何种数据结构和数据库才能使这个检查超级快，以便允许范围内的请求不会拖慢处理该请求的处理速度。另外，如果你将其存储在服务器本身的内存中，那么你就需要保证所有来自给定客户端的请求都会抵达服务器，以便它可以正确地进行限制。为了处理像这样的情形，使用位于服务器之外的独立 <a href="https://en.wikipedia.org/wiki/Redis">Redis 服务</a>非常流行，但是他们将用户详情保存在内存中，这能快速决定用户是否在允许的范围内。</p>
<p>限流可以和你想要的规则一样复杂，但是本节应该涵盖的是基础知识和最常见的用例。</p>
<h2 id="section12">第十二节：消息与发布-订阅</h2>
<p>当你设计和构建大规模 <a href="https://blog.stackpath.com/distributed-system/">分布式系统</a>时，为了让系统可以一起流畅工作，进行系统组件与服务之间信息交换是很重要的。但是如我之前所讲的那样，依赖网络的系统遭受着和网络一样的弱点——它们可经不起折腾。网络故障经常发生。当网络故障时，系统中的组件就无法进行通信，系统可能会降级（最好的情况）或者跟着发生故障（最差的情况）。所以分布式系统需要健壮的机制来确保通信持续进行，或者从停止点恢复，即使系统中的组件之间存在“任意分区”（即故障）也能做到。</p>
<p>举个例子，想象你正预定飞机票。你得到了一个好价钱，选座，确认订票，甚至已经用信用卡付完款。现在你正等待票证的 PDF 被发到你的收件箱。你等呀等，却一直等不到它的出现。系统中的某个地方出现了无法处理或不能正确恢复的故障。订票系统通常会联系航空公司，访问价格 API，处理实际的航班选择——费用汇总、航班的日期和时间等。当你点击网站的预定界面时，一切都会办理妥当。但是，它不必在几分钟后将票证的 PDF 发送给你。相反，界面可以简单地对你的预定进行完成确认，你可以期待票证稍后出现在收件箱中。对订票来说，这是合理也很常规用户体验，因为支付和票证收据不必同时进行——这两个事件是异步的。这样的系统需要发送消息，确认生成 PDF 的异步服务（服务端点）收到已确认的预定付款和所有详情的通知，然后 PDF 可以被自动生成并发送给你。但是如果这个消息系统出现故障，邮件服务永远都不会知道你的预定信息，自然也就不会生成票证了。</p>
<p><strong>发布者/订阅者消息传递</strong></p>
<p>这是一个非常流行的消息范式（模型），关键概念在于：发布者“发布”消息，订阅者订阅消息。为了提供更好的粒度，消息可以属于某个“主题（topic）”，它就像是一个目录。这些主题就像专用的“频道（channel）”或管道一样，每个管道只处理属于特定主题的消息。订阅者选取想要订阅的主题，然后从中获取消息通知。这个系统的优势就是发布者与订阅者之间是完全解耦的——它们不需要了解彼此。发布者播报，订阅者监听其正在寻找的主题的通知。</p>
<p>服务器通常是消息的发布者，它们通常会发布好几种主题（频道）。特定主题的消费者订阅对那些主题进行订阅。服务器（发布者）与订阅者（可以是另一个服务器）之间没有直接的通信。交互只在发布者与主题之间，主题与订阅者之间发生。</p>
<p>主题中的消息只不过是需要通信的数据，可以呈现为任何你需要的形式。这样一来，你的发布/订阅中的就有四个参与者：发布者、订阅者、主题和消息。</p>
<h3 id="">比数据库更好</h3>
<p>所以为什么要在这上面费心呢？为什么不把所有的数据都持久化到数据库，然后直接从数据库消费呢？你需要一个对消息进行排队的系统，因为每个消息都对应着任务，这个任务需要该消息的数据才能完成。所以在我们订票的例子中，如果三十五分钟内有一百个人订票，将所有这些都放到数据库并不能解决给这一百个人发邮件的问题。它只不过是保存一百个事务。发布/订阅（Pub/Sub ）系统处理通信、对任务进行串行排序，并且将消息持久化到数据库。所以这个系统可以提供很多有用的特性，如“至少一次”交付（消息将不会丢失）、持久化存储、消息排序、“重试”、消息的“重放”，等等。若没有这个系统，只是将消息存储在数据库中并不会帮你确保消息被交付（被消费）并成功完成任务。</p>
<p>有时同一个消息可能会不只一次被一个订阅者消费——通常是因为瞬时网络故障，尽管订阅者消费了消息，但是它没有让发布者知道这一点。所以发布者会把消息再次重发给订阅者，这就是为什么保证“至少一次”而不是“有且只有一次”。多次消费的问题在分布式系统中是不可避免的，因为网络天生就是不可靠的。这会让情况变得更复杂，消息在订阅者一侧触发一个操作，那个操作可以改变数据库内的东西（改变整个应用的状态）。如果单个操作被重复多次，并且每次应用的状态都改变了会怎样？</p>
<h3 id="">控制结果——一个或多个结果？</h3>
<p>这个新问题的解决方案被称为幂等性（idempotency）——这是一个很重要的概念，但是在你前几次检查它时没那么直观。这个概念看起来可能很复杂（特别是你读维基百科词条的时候），所以就当前的目的而言，这里是 <a href="https://stackoverflow.com/questions/1077412/what-is-an-idempotent-operation">来自 StackOverflow</a> 的一个更加友好的简化版本：</p>
<blockquote>
<p><em>在计算机中，幂等性指使用相同的参数多次调用某个操作的结果与调用一次的结果相同。</em></p>
</blockquote>
<p>所以当订阅者处理一个消息两次或三次时，应用的整体状态与消息 <em>第一次</em> 被处理后的状态完全相同。例如，如果在预定机票快要完成时，你输入了信用卡信息，由于系统真的太慢了，你点击了三次“立即支付”按钮……你总不想支付三倍的机票钱吧？你需要幂等性确保 <em>第一次</em> 点击后的每次点击都不会产生其它的购买行为，不会不止一次地从你的信用卡扣款。相反，你可以给你最要好的朋友的 newsfeed （译者注：<a href="https://en.wikipedia.org/wiki/News_Feed">newsfeed</a> 是 Facebook 的一个功能）发送 N 次完全相同的评论。它们都将显示为单独的评论，除了令人讨厌之外，它们并没有 <em>错</em>。另一个例子是在 Medium 文章上“鼓掌（clap）”——每次鼓掌都表示增加一次鼓掌的数量，而不是仅仅一次（译者注：<a href="https://help.medium.com/hc/en-us/articles/115011350967-Claps#">Claps</a> 是 Medium 的一个功能，允许你用它来表示对某篇文章的支持，类似于微信朋友圈的点赞）。后面的这两个例子并不要求幂等性，但是支付那个例子是需要的。</p>
<p>有很多不同的消息系统，系统的选取取决于要解决的用例。通常，人们会参考“基于事件的”架构，即系统依赖“事件（event）”（比如支付票证）消息处理操作（比如发送票证邮件）。最常讨论的消息服务有 Apache Kafka、RabbitMQ、Google Cloud Pub/Sub 和 AWS SNS/SQS。</p>
<h2 id="section13">第十三节：必备小知识</h2>
<h3 id="">日志</h3>
<p>随着时间的推移，你的系统会收集大量的数据。大多数数据都是极其有用的，让你了解系统的健康情况、性能和问题，给你提供有价值的见解，帮你了解谁在使用你的系统、如何使用的、多久使用一次、哪些部分被使用得更多或更少，等等。</p>
<p>这些数据对分析、性能优化和产品改进很有价值。它们还有着巨大的调试价值，不论在开发过程中打印到控制台时，还是在测试和生成环境中找出 bug 时，都非常有用。所以日志也帮助你追溯和审计。</p>
<p>记录日志时要记住的要诀就是将它看成一系列连续事件，这意味着数据变成时间序列数据，并且应该使用经过特别设计的工具和数据库来帮你处理此类数据。</p>
<h3 id="">监控</h3>
<p>这是日志之后的下一步。它回答了“我使用所有的日志数据干啥？”这个问题。你监控并分析它。你可以建立或使用解析此类数据的工具和服务，将数据呈现为仪表盘或图表，或其它人类可读的方式。</p>
<p>通过将数据保存到专为此类数据（时间序列数据）设计的数据库中，你可以插入其它基于该数据结构和意图构建的工具。</p>
<h3 id="">预警</h3>
<p>当你积极监控时，也应该安放另一个可以提醒你重大事件的系统。就像一个股票价格超过某个上限或低于某个阈值时出现的警告一样，如果你正在观察的某个指标过高或过低，也可能会发送警报。响应时间（延迟）或错误和故障都是设置警告的好东西，如果它们超过了“可接受”级别，就发送警告。</p>
<p>良好的日志和监控的关键就是确保你的数据随时间推移变得非常一致，因为使用不一致的数据可能导致字段缺失，进而破坏分析工具，或者减少日志带来的好处。</p>
<h2 id="">资源</h2>
<p>正如所承诺的，一些有用的资源如下：</p>
<ol>
<li>一个出色的 <a href="https://github.com/donnemartin/system-design-primer">Github 仓库</a>，充满了概念、图和预学习资料</li>
<li>Tushar Roy 的 <a href="https://www.youtube.com/watch?v=UzLMhqg3_Wc">系统设计介绍</a></li>
<li>Gaurav Sen 的 <a href="https://www.youtube.com/watch?v=quLrc3PbuIw&amp;list=PLMCXHnjXnTnvo6alSjVkgxV-VH6EPyvoX">YouTube 播放列表</a></li>
<li><a href="https://www.sitepoint.com/sql-vs-nosql-differences/">SQL vs NoSQL</a></li>
</ol>
<p>我希望你喜欢这份长篇指南！</p>
<p>你可以<a href="https://twitter.com/ZubinPratap">在 Twitter 上向我提问</a>。</p>
<p><strong>附言：献给 freeCodeCamp 学员</strong></p>
<p>我真的，真的完全相信你最宝贵的资源就是你的时间、精力和金钱。其中，最重要的资源是时间，因为其他两个都可以得到更新或恢复。所以，如果你在某些事情上面花了时间，确保它会让你离目标更进一步。</p>
<p>考虑到这一点，如果你想在我的身上投入三个小时，找出学习编码的最佳路径（特别是如果你像我一样转了行话），那么就去 <a href="https://matchfitmastery.weebly.com/">我的课程网站</a> 填写表单注册吧（不是弹出框！）。如果你在消息（Leave us a message *）中添加 “I LOVE CODE”，我就会知道你是一个 freeCodeCamp 读者，我将你给发送一个优惠码。因为 <a href="https://medium.freecodecamp.org/what-learning-to-code-actually-taught-me-a11fd850af0a">freeCodeCamp</a> 给我了一个良好的开始，就像它帮助你一样。</p>
<p><strong>此外</strong>：如果你想了解更多信息，可以查阅 <a href="http://podcast.freecodecamp.org/">freeCodeCamp 播客</a> 的 <a href="http://podcast.freecodecamp.org/53-zubin-pratap-from-lawyer-to-developer">第 53 集</a>，我在那里同 Quincy（FreeCodeCamp 的创始人）一起分享了我们转行的经验，这些经验可能会对你有所帮助。你也可以访问 <a href="https://itunes.apple.com/au/podcast/ep-53-zubin-pratap-from-lawyer-to-developer/id1313660749?i=1000431046274&amp;mt=2">iTunes</a>、<a href="https://www.stitcher.com/podcast/freecodecamp-podcast/e/59201373?autoplay=true">Stitcher</a> 和 <a href="https://open.spotify.com/episode/4lG0RGpzriG5vXRMgza05C">Spotify</a> 上的播客。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/systems-design-for-interviews/">System Design Interview Questions – Concepts You Should Know</a>，作者：<a href="https://www.freecodecamp.org/news/author/zubin/">Zubin Pratap</a></p></figure> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ SQL 连接教程：交叉连接、全外连接、内连接、左连接和右连接 ]]>
                </title>
                <description>
                    <![CDATA[ SQL 连接让关系型数据库管理系统名副其实。 连接（join）允许我们将分离的数据表重新构造成驱动应用程序的关系（relationship）。 本文将逐一查看 SQL 中的各种连接及其使用方式。 这里是我们将要讨论的内容：  * 什么是连接？  * 设置数据库  * 交叉连接  * 准备示例数据（导演和电影）  * 左连接与右连接  * 全外连接  * 内连接  * 使用左连接过滤数据  * 多表连接  * 带额外条件的连接  * 连接查询的现实 （剧透警告：我们将介绍五种不同类型的连接，但实际上你只需要知道其中两种！） 什么是连接 连接  是一种将两行结合成一行的操作。 这些行通常来自不同的表，但这并不绝对。 在我们着眼于如何写连接之前，我们先看一看连接的结果长啥样。 我们就以一个保存用户信息和他们的地址信息的系统为例吧。 保存用户信息的表里面的行可能长这个样子：  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/sql-joins-tutorial/</link>
                <guid isPermaLink="false">5ff27c9039641a0517d531dd</guid>
                
                    <category>
                        <![CDATA[ SQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 数据库 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Mon, 04 Jan 2021 02:53:01 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/1609728756536.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>SQL 连接让关系型数据库管理系统名副其实。</p>
<p>连接（join）允许我们将分离的数据表重新构造成驱动应用程序的关系（relationship）。</p>
<p>本文将逐一查看 SQL 中的各种连接及其使用方式。</p>
<p>这里是我们将要讨论的内容：</p>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E8%BF%9E%E6%8E%A5">什么是连接？</a></li>
<li><a href="#%E8%AE%BE%E7%BD%AE%E6%95%B0%E6%8D%AE%E5%BA%93">设置数据库</a></li>
<li><a href="#%E4%BA%A4%E5%8F%89%E8%BF%9E%E6%8E%A5">交叉连接</a></li>
<li><a href="#%E5%87%86%E5%A4%87%E7%A4%BA%E4%BE%8B%E6%95%B0%E6%8D%AE%EF%BC%88%E5%AF%BC%E6%BC%94%E5%92%8C%E7%94%B5%E5%BD%B1%EF%BC%89">准备示例数据（导演和电影）</a></li>
<li><a href="#%E5%B7%A6%E8%BF%9E%E6%8E%A5%E4%B8%8E%E5%8F%B3%E8%BF%9E%E6%8E%A5">左连接与右连接</a></li>
<li><a href="#%E5%85%A8%E5%A4%96%E8%BF%9E%E6%8E%A5">全外连接</a></li>
<li><a href="#%E5%86%85%E8%BF%9E%E6%8E%A5">内连接</a></li>
<li><a href="#%E4%BD%BF%E7%94%A8%E5%B7%A6%E8%BF%9E%E6%8E%A5%E8%BF%87%E6%BB%A4%E6%95%B0%E6%8D%AE">使用左连接过滤数据</a></li>
<li><a href="#%E5%A4%9A%E8%A1%A8%E8%BF%9E%E6%8E%A5">多表连接</a></li>
<li><a href="#%E5%B8%A6%E9%A2%9D%E5%A4%96%E6%9D%A1%E4%BB%B6%E7%9A%84%E8%BF%9E%E6%8E%A5">带额外条件的连接</a></li>
<li><a href="#%E7%8E%B0%E5%AE%9E%E4%B8%AD%E7%9A%84%E8%BF%9E%E6%8E%A5%E6%9F%A5%E8%AF%A2">连接查询的现实</a></li>
</ul>
<p>（<em>剧透警告</em>：我们将介绍五种不同类型的连接，但实际上你只需要知道其中两种！）</p>
<h2 id="">什么是连接</h2>
<p><strong>连接</strong> 是一种将两行结合成一行的操作。</p>
<p>这些行通常来自不同的表，但这并不绝对。</p>
<p>在我们着眼于如何写连接之前，我们先看一看连接的结果长啥样。</p>
<p>我们就以一个保存用户信息和他们的地址信息的系统为例吧。</p>
<p>保存用户信息的表里面的行可能长这个样子：</p>
<pre><code> id |     name     |        email        | age
----+--------------+---------------------+-----
  1 | John Smith   | johnsmith@gmail.com |  25
  2 | Jane Doe     | janedoe@Gmail.com   |  28
  3 | Xavier Wills | xavier@wills.io     |  3
...
(7 rows)
</code></pre>
<p>保存地址信息的表里面的行可能是这个样子：</p>
<pre><code> id |      street       |     city      | state | user_id
----+-------------------+---------------+-------+---------
  1 | 1234 Main Street  | Oklahoma City | OK    |       1
  2 | 4444 Broadway Ave | Oklahoma City | OK    |       2
  3 | 5678 Party Ln     | Tulsa         | OK    |       3
(3 rows)
</code></pre>
<p>我们可以写多个独立的查询，既检索用户信息，又检索地址信息。但是在理想情况下，我们可以写 <em>一条查询</em>，然后用同一个结果集（result set）接收所有的用户和他们的地址信息。</p>
<p>这正是连接让我们做的事情！</p>
<p>我们很快就会看到如何写这些连接，但如果我们连接用户信息表和地址信息表，就可以得到一个像这样的结果：</p>
<pre><code> id |     name     |        email        | age | id |      street       |     city      | state | user_id
----+--------------+---------------------+-----+----+-------------------+---------------+-------+---------
  1 | John Smith   | johnsmith@gmail.com |  25 |  1 | 1234 Main Street  | Oklahoma City | OK    |       1
  2 | Jane Doe     | janedoe@Gmail.com   |  28 |  2 | 4444 Broadway Ave | Oklahoma City | OK    |       2
  3 | Xavier Wills | xavier@wills.io     |  35 |  3 | 5678 Party Ln     | Tulsa         | OK    |       3
(3 rows)

</code></pre>
<p>在这里，我们可以在一个不错的结果集中看到用户和他们的地址。</p>
<p>除了产生组合结果集，连接的另一个重要用途就是为查询语句提供额外的信息，便于对结果集进行过滤。</p>
<p>例如，如果我们想给住在 Oklahoma 市的所有用户发送实体邮件，我们就可以使用这个连接在一起的结果集，然后根据 <code>city</code> 列进行过滤。</p>
<p>既然我们已经知道了连接的用途，那就开始写一些吧！</p>
<h2 id="">设置数据库</h2>
<p>在写查询语句之前，我们需要先设置好数据库。</p>
<p>在这些示例中，我们将使用 PostgreSQL，但是这里展示的查询和概念可以很容易地被翻译到任何其它的现代数据库系统中（比如 MySQL、SQL Server，等等）。</p>
<p>我们可以使用 <a href="https://www.postgresql.org/docs/current/app-psql.html"><code>psql</code></a>与 PostgreSQL 数据库一起工作，它是 PostgreSQL 的交互式命令行程序。如果你有其它的数据库客户端，你也可以用它们。</p>
<p>首先，创建我们的数据库。[安装]好 PostgreSQL 之后，在终端运行 <code>createdb &lt;database-name&gt;</code>命令，创建一个新的数据库。我管我的数据库叫 <code>fcc</code>：</p>
<pre><code class="language-bash">$ createdb fcc
</code></pre>
<p>接下来使用命令 <code>psql</code> 启动一个交互式控制台，然后使用 <code>\c &lt;database-name&gt;</code> 连接到我们刚才创建的数据库：</p>
<pre><code class="language-bash">$ psql
psql (11.5)
Type "help" for help.

john=# \c fcc
You are now connected to database "fcc" as user "john".
fcc=#
</code></pre>
<blockquote>
<p><strong>注意：</strong> 为了便于阅读，我已经去掉了这些示例中 <code>psql</code> 的输出，所以如果你的终端显示的内容和这里的不完全相同，也不必担心。</p>
</blockquote>
<p>我鼓励你亲自动手跟着这些示例来一遍。如果你完成这些示例，你学到和记住的东西要比只阅读来的收获多得多。</p>
<p>现在看连接！</p>
<h2 id="">交叉连接</h2>
<p>交叉连接（<code>CROSS JOIN</code>）是我们能做的最简单的连接，它又叫 <em>笛卡尔乘积（Cartesian product）</em>。</p>
<p>这种连接将一张表中的每一行与另一张表中的每一行逐一进行连接。</p>
<p>如果我们有两个列表——一个包含 <code>1、2、3</code>，另一个包含 <code>A、B、C</code>。这两个列表的笛卡尔乘积就是这样：</p>
<pre><code>1A, 1B, 1C
2A, 2B, 2C
3A, 3B, 3C
</code></pre>
<p>第一个列表中的每一个值都与第二个列表中的每一个值进行了配对。</p>
<p>让我们把这个示例写成一条 SQL 查询。</p>
<p>我们先创建两个非常简单的表，再插入一些数据进去：</p>
<pre><code class="language-sql">CREATE TABLE letters(
  letter TEXT
);

INSERT INTO letters(letter) VALUES ('A'), ('B'), ('C');

CREATE TABLE numbers(
  number TEXT
);

INSERT INTO numbers(number) VALUES (1), (2), (3);
</code></pre>
<p>我们的两个表（<code>letters</code> 和 <code>numbers</code>）都只有一列：一个简单的文本字段。</p>
<p>现在使用 <code>CROSS JOIN</code> 将它们连接在一起：</p>
<pre><code class="language-sql">SELECT *
FROM letters
CROSS JOIN numbers;
</code></pre>
<pre><code> letter | number
--------+--------
 A      | 1
 A      | 2
 A      | 3
 B      | 1
 B      | 2
 B      | 3
 C      | 1
 C      | 2
 C      | 3
(9 rows)

</code></pre>
<p>这是我们能做的最简单的连接，但是即使是在这个简单的示例中，我们也能看见连接在起作用：分开的两行（一行来自 <code>letter</code>，一行来自 <code>numbers</code>）已经被 <em>连接</em> 成一行。</p>
<p>虽然这种连接通常只被当作学术范例进行讨论，但它至少有一个很好的用例：覆盖日期范围。</p>
<h3 id="">带日期范围的交叉连接</h3>
<p>交叉连接的一个好用例就是从表中读取每一行，然后将其用于某个日期范围内的每一天。</p>
<p>例如你正在构建一个追踪每日任务的应用，如刷牙、吃早饭或洗澡。</p>
<p>如果你想为上周 <em>每天</em> 和 <em>每个任务</em> 都生成一条记录，你可以在某个日期范围上使用 <code>CROSS JOIN</code>。</p>
<p>我们可以使用 <a href="https://www.postgresql.org/docs/current/functions-srf.html"><code>generate_series</code></a> 函数产生这个日期范围：</p>
<pre><code class="language-sql">SELECT generate_series(
  (CURRENT_DATE - INTERVAL '5 day'),
  CURRENT_DATE,
  INTERVAL '1 day'
)::DATE AS day;

</code></pre>
<p><code>generate_series</code> 函数接收三个参数。</p>
<p>第一个参数是起始值，我们在示例中使用的是 <code>CURRENT_DATE - INTERVAL '5 day'</code>，它返回当前日期减去五天（五天前）的值。</p>
<p>第二个参数是当前日期（<code>CURRENT_DATE</code>）。</p>
<p>第三个参数是阶距（step interval），也就是值每次的增量大小。因为这些是日常任务，所以我们使用一天做为间隔（<code>INTERVAL '1 day'</code>）。</p>
<p>把这三个参数放在一起，就会生成一个日期序列：从五天前开始，到今天结束，每次前进一天。</p>
<p>最后，我们通过使用 <code>::DATE</code> 将这些值的输出转为日期类型，去掉时间部分。我们还使用 <code>AS day</code> 为这一列设置了别名，让输出看起来更友好。</p>
<p>这个查询的输出就是过去五天加今天：</p>
<pre><code>    day
------------
 2020-08-19
 2020-08-20
 2020-08-21
 2020-08-22
 2020-08-23
 2020-08-24
(6 rows)
</code></pre>
<p>回到我们的每日任务示例，一起创建一个存放我们想要完成的任务的简单表吧，然后插入几个任务：</p>
<pre><code class="language-sql">CREATE TABLE tasks(
  name TEXT
);

INSERT INTO tasks(name) VALUES
('Brush teeth'),
('Eat breakfast'),
('Shower'),
('Get dressed');
</code></pre>
<p><code>task</code> 表只有一列（<code>name</code>），我们往这个表中插入了四个任务。</p>
<p>现在让我们把任务和生成日期的查询交叉连接在一起吧：</p>
<pre><code class="language-sql">SELECT
  tasks.name,
  dates.day
FROM tasks
CROSS JOIN
(
  SELECT generate_series(
    (CURRENT_DATE - INTERVAL '5 day'),
    CURRENT_DATE,
    INTERVAL '1 day'
  )::DATE    AS day
) AS dates

</code></pre>
<p>（因为我们生成日期的查询并不是一个真实的表，所以我们将它写成了子查询。）</p>
<p>这个查询返回了任务名和日期，结果集看起来像这样：</p>
<pre><code>     name      |    day
---------------+------------
 Brush teeth   | 2020-08-19
 Brush teeth   | 2020-08-20
 Brush teeth   | 2020-08-21
 Brush teeth   | 2020-08-22
 Brush teeth   | 2020-08-23
 Brush teeth   | 2020-08-24
 Eat breakfast | 2020-08-19
 Eat breakfast | 2020-08-20
 Eat breakfast | 2020-08-21
 Eat breakfast | 2020-08-22
 ...
 (24 rows)

</code></pre>
<p>不出所料，日期范围内的每一天中的每一个任务都对应着一行。</p>
<p><code>CROSS JOIN</code> 是我们能使用的最简单的连接。但是，为了查看接下来的几种类型的连接，我们需要设置更加真实的表。</p>
<h2 id="">准备示例数据（导演和电影）</h2>
<p>为了展示接下来的几种连接类型，我们需要使用 <em>电影</em> 和 <em>电影导演</em> 这个例子。</p>
<p>在这种情况下，一部电影有一位导演，但是这不是 <em>必需</em> 的——想像一部新电影被宣布，但是它的导演还未确定。</p>
<p>我们的演员表（<code>directors</code>）将会保存每位演员的姓名，电影表（<code>movie</code>）将会保存每部电影的名字以及一个指向导演的引用（如果有的话）。</p>
<p>咱们先创建那两个表并插入一些数据吧：</p>
<pre><code class="language-sql">CREATE TABLE directors(
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL
);

INSERT INTO directors(name) VALUES
('John Smith'),
('Jane Doe'),
('Xavier Wills'),
('Bev Scott'),
('Bree Jensen');

CREATE TABLE movies(
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  director_id INTEGER REFERENCES directors 
);

INSERT INTO movies(name, director_id) VALUES
('Movie 1', 1),
('Movie 2', 1),
('Movie 3', 2),
('Movie 4', NULL),
('Movie 5', NULL);
</code></pre>
<p>我们有五位导演和五部电影，其中三部电影是有导演的。ID 为 1 的导演有两部电影，ID 为 2 的导演有一部电影。</p>
<h2 id="">全外连接</h2>
<p>既然已经有了一些数据，我们就看看全外连接（<code>FULL OUTER JOIN</code>）吧。</p>
<p><code>FULL OUTER JOIN</code> 与 <code>CROSS JOIN</code> 有些类似，但又有两个关键的区别。</p>
<p>第一个区别是 <code>FULL OUTER JOIN</code> 需要有一个 <strong>连接条件（join condition）</strong>。</p>
<p>连接条件声明了两张表的行与行之间是如何关联的，以及将它们连接在一起的条件。</p>
<p>在我们的示例中，<code>movies</code> 表通过 <code>director_id</code> 列引用演员，这一列与 <code>directors</code> 表的 <code>id</code> 列相匹配。我们将把这两列用作连接条件。</p>
<p>这是我们用来连接两张表的 SQL：</p>
<pre><code class="language-sql">SELECT *
FROM movies
FULL OUTER JOIN directors
  ON directors.id = movies.director_id;
</code></pre>
<p>注意：我们将匹配电影和导演的连接连接声明为：<code>ON movie.director_id = directors.id</code>。</p>
<p>我们的结果集看起来就像一个奇怪的笛卡尔乘积：</p>
<pre><code>  id  |  name   | director_id |  id  |     name
------+---------+-------------+------+--------------
    1 | Movie 1 |           1 |    1 | John Smith
    2 | Movie 2 |           1 |    1 | John Smith
    3 | Movie 3 |           2 |    2 | Jane Doe
    4 | Movie 4 |        NULL | NULL | NULL
    5 | Movie 5 |        NULL | NULL | NULL
 NULL | NULL    |        NULL |    5 | Bree Jensen
 NULL | NULL    |        NULL |    4 | Bev Scott
 NULL | NULL    |        NULL |    3 | Xavier Wills
(8 rows)
</code></pre>
<p>我们首先看到的是那些有导演的电影对应的行，这时连接条件的值为真。</p>
<p>然而，我们还能在这些行后面看到 <em>每张表</em> 中剩余的行，只是未匹配上的另一张表的对应值为 <code>NULL</code>。</p>
<blockquote>
<p>**注意：**如果你不熟悉 <code>NULL</code> 值，可以从<a href="https://www.freecodecamp.org/news/sql-operators-tutorial/#dealing-with-missing-data-null-"> SQL 运算符教程</a>中查看我的解释。</p>
</blockquote>
<p>我们还看到了 <code>CROSS JOIN</code> 与 <code>FULL OUTER JOIN</code> 的另一个区别：对于每张表，<code>FULL OUTER JOIN</code> 只会返回一行，而 <code>CROSS JOIN</code> 会返回多行。</p>
<h2 id="">内连接</h2>
<p>下一种连接类型——内连接（<code>INNER JOIN</code>），是最常用的连接类型之一。</p>
<p>内连接 <strong>只返回连接条件为真的那些行</strong>。</p>
<p>在我们的示例中，<code>movies</code> 和 <code>directors</code> 表之间的内连接只会返回指派了导演的电影的记录。</p>
<p>语法与之前的基本一致：</p>
<pre><code class="language-sql">SELECT *
FROM movies
INNER JOIN directors
  ON directors.id = movies.director_id;
</code></pre>
<p>我们的结果展示了有导演的那三部电影：</p>
<pre><code> id |  name   | director_id | id |    name
----+---------+-------------+----+------------
  1 | Movie 1 |           1 |  1 | John Smith
  2 | Movie 2 |           1 |  1 | John Smith
  3 | Movie 3 |           2 |  2 | Jane Doe
(3 rows)
</code></pre>
<p>由于内连接只包含满足连接条件的那些行，所以 <em>连接两个表时，谁先谁后并不重要</em>。</p>
<p>如果我们反转查询中表的顺序，我们还是会得到相同的结果：</p>
<pre><code class="language-sql">SELECT *
FROM directors
INNER JOIN movies
  ON movies.director_id = directors.id;
</code></pre>
<pre><code> id |    name    | id |  name   | director_id
----+------------+----+---------+-------------
  1 | John Smith |  1 | Movie 1 |           1
  1 | John Smith |  2 | Movie 2 |           1
  2 | Jane Doe   |  3 | Movie 3 |           2
(3 rows)
</code></pre>
<p>在这个查询中，由于我们先列出了 <code>directors</code> 表并选取所有的列（<code>SELECT *</code>），所以我们会先看到 <code>directors</code> 表中的列，然后才是 <code>movies</code> 表中的列，但是得到的数据是一样的。</p>
<p>内连接的这个性质非常有用，但是它并不适用于所有的连接类型——比如接下来的这种类型。</p>
<h2 id="">左连接与右连接</h2>
<p>接下来的两种连接类型用了一个修饰符（<code>LEFT</code> 或 <code>RIGHT</code>），它会决定哪个表的数据被包含到结果集中。</p>
<blockquote>
<p><strong>注意：</strong> <code>LEFT JOIN</code> 和 <code>RIGHT JOIN</code> 也可以被称为 <code>LEFT OUTER JOIN</code> 和 <code>RIGHT OUTER JOIN</code>。</p>
</blockquote>
<p>这两种连接的使用场景是：我们想返回某个特定表中的所有数据，<em>如果关联表中存在相应的数据</em>，也将其返回。</p>
<p>如果关联的数据不存在，我们仍然可以得到“主表”的所有记录。</p>
<p>它是一个针对特定事物的信息和奖励信息（如果存在的话）的查询。</p>
<p>举个例子就好理解了。让我们找出所有的电影和它们的导演，但是我们不关心电影是否有导演——导演就是奖励：</p>
<pre><code class="language-sql">SELECT *
FROM movies
LEFT JOIN directors
  ON directors.id = movies.director_id;
</code></pre>
<p>这个查询的形式与之前一样——我们只是将连接声明为了 <code>LEFT JOIN</code>。</p>
<p>在这个示例中，<code>movies</code> 表就是“左表”。</p>
<p>如果我们将查询写成一行，看起来会更容易一些：</p>
<pre><code class="language-sql">... FROM movies LEFT JOIN directors ...
</code></pre>
<p><strong>左连接返回“左表”中的所有记录</strong>。</p>
<p>左连接会返回“右表”中 <strong>满足连接条件</strong> 的所有记录。</p>
<p>“右表”中 <strong>不满足连接条件的记录会以 <code>NULL</code> 值返回</strong>。</p>
<pre><code> id |  name   | director_id |  id  |    name
----+---------+-------------+------+------------
  1 | Movie 1 |           1 |    1 | John Smith
  2 | Movie 2 |           1 |    1 | John Smith
  3 | Movie 3 |           2 |    2 | Jane Doe
  4 | Movie 4 |        NULL | NULL | NULL
  5 | Movie 5 |        NULL | NULL | NULL
(5 rows)
</code></pre>
<p>看着这个结果集，我们就能明白为何这种连接在 <em>所有的这个和部分的那个（如果存在的话）</em> 类型的连接中特别有用了。</p>
<h3 id="">右连接</h3>
<p>除了调换了关于两表的要求外，右连接（<code>RIGHT JOIN</code>）的工作原理与 <code>LEFT JOIN</code> 一模一样。</p>
<p>在右连接中，“右表”中的所有行全部返回，而“左表”根据连接条件返回。</p>
<p>我们还是使用上面的那个查询，只不过这一次要将 <code>LEFT JOIN</code> 替换成 <code>RIGHT JOIN</code>：</p>
<pre><code class="language-sql">SELECT *
FROM movies
RIGHT JOIN directors
  ON directors.id = movies.director_id;
</code></pre>
<pre><code>  id  |  name   | director_id | id |     name
------+---------+-------------+----+--------------
    1 | Movie 1 |           1 |  1 | John Smith
    2 | Movie 2 |           1 |  1 | John Smith
    3 | Movie 3 |           2 |  2 | Jane Doe
 NULL | NULL    |        NULL |  5 | Bree Jensen
 NULL | NULL    |        NULL |  4 | Bev Scott
 NULL | NULL    |        NULL |  3 | Xavier Wills
(6 rows)

</code></pre>
<p>我们现在的结果集返回了 <code>directors</code> 表的所有行和 <code>movies</code> 数据（如果存在的话）。</p>
<p>我们所做的就是切换“主表”，“主表”就是不管数据存在与否，我们都想查看到所有数据的那个表。</p>
<h3 id="">生产应用程序中的左连接与右连接</h3>
<p>在生产应用程序中，我只用过 <code>LEFT JOIN</code>，从未用过 <code>RIGHT JOIN</code>。</p>
<p>我这么做的原因是：我认为 <code>LEFT JOIN</code> 让查询更易于阅读和理解。</p>
<p>当我写查询语句时，我喜欢从“基础”结果集开始思考，比如所有的电影。然后在它的基础上引入（或去除）一些东西。</p>
<p>因为我喜欢从基础开始，而 <code>LEFT JOIN</code> 恰好满足了这种思路。我想要的是基础表（“左表”）中的所有行和“右表”中满足条件的行。</p>
<p>在实践中，我觉得我没有在生产应用中见过 <code>RIGHT JOIN</code>。<code>RIGHT JOIN</code> 并没有错，我只是认为它会让查询更难理解。</p>
<h3 id="">重写右连接</h3>
<p>如果我们想翻转上面的场景，返回所有的导演和满足条件的电影，我们可以将 <code>RIGHT JOIN</code> 重写为 <code>LEFT JOIN</code>。</p>
<p>我们只需要翻转两个表在查询语句中的顺序，再把 <code>RIGHT</code> 改成 <code>LEFT</code> 即可：</p>
<pre><code class="language-sql">SELECT *
FROM directors
LEFT JOIN movies
  ON movies.director_id = directors.id;
</code></pre>
<blockquote>
<p><strong>注意：</strong> 我喜欢将被连接的表（“右表”，即上面示例中的 <code>movies</code>）写在连接条件中的第一个（(<code>ON movies.director_id = ...</code>），不过这只是我的个人偏好。</p>
</blockquote>
<h2 id="">使用左连接过滤数据</h2>
<p><code>LEFT JOIN</code>（或 <code>RIGHT JOIN</code>）有两个用例。</p>
<p>第一个用例我们已经讲了：返回一个表中的所有数据，以及另一个表中满足条件的数据。</p>
<p>第二个用例是 <strong>在第二个表中的数据不存在的情况下</strong>，返回第一个表中的所有行。</p>
<p>这个场景看起来像这样：找出 <em>不属于任何电影</em> 的导演。</p>
<p>为此，我们从 <code>LEFT JOIN</code> 开始，将 <code>directors</code> 作为主表或“左表”：</p>
<pre><code class="language-sql">SELECT *
FROM directors
LEFT JOIN movies
  ON movies.director_id = directors.id;
</code></pre>
<p>对于那些不属于任何电影的导演来说，来自 <code>movies</code> 表的列都是 <code>NULL</code>：</p>
<pre><code> id |     name     |  id  |  name   | director_id
----+--------------+------+---------+-------------
  1 | John Smith   |    1 | Movie 1 |           1
  1 | John Smith   |    2 | Movie 2 |           1
  2 | Jane Doe     |    3 | Movie 3 |           2
  5 | Bree Jensen  | NULL | NULL    |        NULL
  4 | Bev Scott    | NULL | NULL    |        NULL
  3 | Xavier Wills | NULL | NULL    |        NULL
(6 rows)
</code></pre>
<p>在我们的示例中，ID 为 3、4 和 5 的导演不属于任何一部电影。</p>
<p>要从结果集中过滤出这些行，我们可以加一个 <code>WHERE</code> 子句，只返回电影数据为 <code>NULL</code> 的行：</p>
<pre><code class="language-sql">SELECT *
FROM directors
LEFT JOIN movies
  ON movies.director_id = directors.id
WHERE movies.id IS NULL;
</code></pre>
<pre><code> id |     name     |  id  | name | director_id
----+--------------+------+------+-------------
  5 | Bree Jensen  | NULL | NULL |        NULL
  4 | Bev Scott    | NULL | NULL |        NULL
  3 | Xavier Wills | NULL | NULL |        NULL
(3 rows)
</code></pre>
<p>有三部电影没有导演！</p>
<p>使用表的 <code>id</code> 列进行过滤（<code>WHERE movies.id IS NULL</code>）是很普遍的。但是，因为 <code>movies</code> 表的所有列都是 <code>NULL</code>，所以使用其中任何一个都能达到目的。</p>
<p>（由于我们知道 <code>movies</code> 表中的所有列都会是 <code>NULL</code>，所以在上面的那个查询中，我们可以将 <code>SELECT *</code> 替换成 <code>SELECT directors.*</code>，从而只返回所有的导演信息）。</p>
<h3 id="">使用左连接查找匹配</h3>
<p>在我们之前的查询中，我们找到了那些 <em>不</em> 属于任何电影的导演。</p>
<p>使用同样的结构，我们可以找到那些 <em>肯定</em> 属于电影的导演。只需要将 <code>WHERE</code> 条件改成寻找电影数据 <em>不是</em> <code>NULL</code> 的电影数据即可：</p>
<pre><code class="language-sql">SELECT *
FROM directors
LEFT JOIN movies
  ON movies.director_id = directors.id
WHERE movies.id IS NOT NULL;
</code></pre>
<pre><code> id |    name    | id |  name   | director_id
----+------------+----+---------+-------------
  1 | John Smith |  1 | Movie 1 |           1
  1 | John Smith |  2 | Movie 2 |           1
  2 | Jane Doe   |  3 | Movie 3 |           2
(3 rows)
</code></pre>
<p>这可能看起来很方便，但我们实际上只是在重新实现 <code>INNER JOIN</code> 而已。</p>
<h2 id="">多表连接</h2>
<p>我们已经看到如何将两张表连接在一起了，但对于多表连接会怎么样呢？</p>
<p>实际上很简单，但是我们需要第三个表（<code>tickets</code>）来进行举例：</p>
<p>这个表将代表已经售出的电影票：</p>
<pre><code class="language-sql">CREATE TABLE tickets(
  id SERIAL PRIMARY KEY,
  movie_id INTEGER REFERENCES movies NOT NULL
);

INSERT INTO tickets(movie_id) VALUES (1), (1), (3);
</code></pre>
<p><code>tickets</code> 表只有一个 <code>id</code> 和一个电影的引用：<code>movie_id</code>。</p>
<p>我们也为 ID 为 1 的电影插入了两张票，为 ID 为 3 的电影插入了一张票。</p>
<p>现在，让我们连接 <code>directors</code> 和 <code>movies</code>，然后再连接 <code>movies</code> 和 <code>tickets</code>！</p>
<pre><code class="language-sql">SELECT *
FROM directors
INNER JOIN movies
  ON movies.director_id = directors.id
INNER JOIN tickets
  ON tickets.movie_id = movies.id;
</code></pre>
<p>因为这些是内连接，所以连接的顺序并不重要。我们可以从 <code>tickets</code> 开始，连接 <code>movies</code>，再连接 <code>directors</code>。</p>
<p>它的顺序也取决于你想要查询的什么以及什么让查询最易于理解。</p>
<p>在结果集中，我们将会注意到返回行的范围已经被进一步缩小了：</p>
<pre><code> id |    name    | id |  name   | director_id | id | movie_id
----+------------+----+---------+-------------+----+----------
  1 | John Smith |  1 | Movie 1 |           1 |  1 |        1
  1 | John Smith |  1 | Movie 1 |           1 |  2 |        1
  2 | Jane Doe   |  3 | Movie 3 |           2 |  3 |        3
(3 rows)
</code></pre>
<p>这是因为我们加了另一个 <code>INNER JOIN</code>，实际上它给我们的查询添加了另一个 <code>AND</code> 条件。</p>
<p>我们的查询本质上是在说：<em>“返回所有属于电影的导演，这些电影 <strong>也有</strong> 电影票售出”</em>。</p>
<p>如果我们想找出属于 <em>可能还没有电影票售出</em> 的电影的导演，我们可以将最后一个 <code>INNER JOIN</code> 替换成 <code>LEFT JOIN</code>：</p>
<pre><code class="language-sql">SELECT *
FROM directors
JOIN movies
  ON movies.director_id = directors.id
LEFT JOIN tickets
  ON tickets.movie_id = movies.id;
</code></pre>
<p>我们可以看到 <code>Movie 2</code> 现在又回到结果集中来了：</p>
<pre><code> id |    name    | id |  name   | director_id |  id  | movie_id
----+------------+----+---------+-------------+------+----------
  1 | John Smith |  1 | Movie 1 |           1 |    1 |        1
  1 | John Smith |  1 | Movie 1 |           1 |    2 |        1
  2 | Jane Doe   |  3 | Movie 3 |           2 |    3 |        3
  1 | John Smith |  2 | Movie 2 |           1 | NULL |     NULL
(4 rows)

</code></pre>
<p>这部电影一张票也没有卖出去，所以它因 <code>INNER JOIN</code> 被之前的结果集中排除掉了。</p>
<p>我将 <em>给读者留一个练习</em>，你如何能找出那些属于一张票也 <strong>没有</strong> 卖出去的电影的导演呢？</p>
<h3 id="">连接的执行顺序</h3>
<p>最后，我们并不关心连接是以何种顺序执行的。</p>
<p>SQL 和其它现代编程语言之间的一个关键区别就是 SQL 是一门 <strong>声明式</strong> 语言。</p>
<p>这意味着我们声明期望的结果，但并不声明执行细节——这些细节留给了给数据库查询规划器（database query planner）。我们只管声明我们想要的连接和条件，查询规划器会处理剩余部分。</p>
<p>但是，现实中的数据库并不会同时连接三张表。相反，它会先连接前两张表得到一个中间结果，然后把这个中间结果集与第三张表连接。</p>
<p>（<strong>注意：</strong> 这个解释有点简单。）</p>
<p>因此，当我们在查询中遇到多表连接时，我们可以将它们看成一连串的两表连接——尽管其中的一个表会变得非常大。</p>
<h2 id="">带额外条件的连接</h2>
<p>我们将要介绍的最后一个主题是带额外条件的连接。</p>
<p>和 <code>WHERE</code> 字句类似，我们可以添加任意多的条件到连接中。</p>
<p>例如，如果我们想找出电影和导演的信息，但导演的 <em>名字</em> 不能是 <em>“John Smith”</em>。我们可以用 <code>AND</code> 添加一个额外的条件到我们的连接中：</p>
<pre><code class="language-sql">SELECT *
FROM movies
INNER JOIN directors
  ON directors.id = movies.director_id
  AND directors.name &lt;&gt; 'John Smith';
</code></pre>
<p>我们可以在这个连接条件中使用任何放在 <code>WHERE</code> 子句中的运算符。</p>
<p>如果我们将条件放到 <code>WHERE</code> 子句中，也能得到同样的结果：</p>
<pre><code class="language-sql">SELECT *
FROM movies
INNER JOIN directors
  ON directors.id = movies.director_id
WHERE directors.name &lt;&gt; 'John Smith';
</code></pre>
<p>这里背后发生的事情有一些细微的差异，但是就本文而言，结果集是一样的。</p>
<p>（如果你不太熟悉过滤 SQL 查询的所有方式，你可以在<a href="https://www.freecodecamp.org/news/sql-operators-tutorial/">这里</a>查看之前提到的文章。）</p>
<h2 id="">现实中的连接查询</h2>
<p>事实上，我发现我自己只通过三种方式使用连接：</p>
<h4 id="">内连接</h4>
<p>第一个用例是两表之间的关系 <strong>肯定</strong> 存在的记录，通过 <code>INNER JOIN</code> 实现。</p>
<p>条件如查找 “<em>具有导演的电影</em>” 或 “<em>具有帖子的用户</em>”。</p>
<h4 id="">左连接</h4>
<p>第二个用例是来自一个表的记录，以及关系存在时来自第二个表的记录，通过 <code>LEFT JOIN</code> 实现。</p>
<p>条件如 <em>“具有导演的电影（如果有的话）”</em> 或 “<em>具有文章的用户（如果有的话）</em>”。</p>
<h4 id="">左连接排除数据</h4>
<p>第三个常见的用例是 <code>LEFT JOIN</code> 的第二个用例：查找一个表中的记录，但这些记录在另一个表中 <strong>没有</strong> 关系。</p>
<p>条件如 “<em>没有导演的电影</em>” 或 “<em>没有文章的用户</em>”。</p>
<h3 id="">两种非常有用的连接类型</h3>
<p>我认为我不曾在生产应用中用过 <code>FULL OUTER JOIN</code> 或 <code>RIGHT JOIN</code>，相关用例要么很少，要么查询可以被写成更加清晰的形式（<code>RIGHT JOIN</code> 的情况）。</p>
<p>我偶尔会为一些东西使用 <code>CROSS JOIN</code>，比如分散在某个时间范围内的记录（像我们一开始看到的那样），但是那种场景也不多。</p>
<p>所以，好消息来了！对于你将遇到的 99.9% 的用例，只需要理解 <code>INNER JOIN</code> 和 <code>LEFT JOIN</code> 这两种类型的连接就可以解决了。</p>
<p>如果你喜欢这篇文章，可以在 <a href="https://twitter.com/johnmosesman">twitter</a> 上关注我，我在那里谈论数据库和所有其它开发相关的话题。</p>
<p>感谢阅读！</p>
<p>John</p>
<p><strong>P.S.</strong> 文末小技巧：大多数数据库系统允许你用 <code>JOIN</code> 代替 <code>INNER JOIN</code>——可以让你少打几个字。:)</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/sql-joins-tutorial/">SQL Joins Tutorial: Cross Join, Full Outer Join, Inner Join, Left Join, and Right Join</a>，作者：<a href="https://www.freecodecamp.org/news/author/johnmosesman/">John Mosesman</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Java 多态教程 ]]>
                </title>
                <description>
                    <![CDATA[ 多态（Polymorphism）的字面意思是“多种形式”，它允许对象被视为可替代的。当你想让不同类型的对象完成相同的动作时，多态能帮你减少重复代码。 让我们解释一下这到底是什么意思。 用类比解释多态 如果你有过国际旅行的经历，你的行李检查单上很可能会有电插头适配器。否则，你可能就无法给你的手机和其它设备充电了。 Call Me Fred [https://unsplash.com/@callmefred?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText]   拍摄  很奇怪，世界上大概有 16 种不同类型的电源插座：两脚的、三脚的、圆形的、方形的，插脚的配置也五花八门。 大多数人的对策就是买一个通用的插头适配器。 换一种方式看待这个问题，普遍问题是我们的插座接口只接受一种类型的插头对象！插座不是多态的。 要是我们的插座能接受很多不同类型的插头，生活就没那么累了。我们可以通过创造不同形状的开口，让插座变成多态的，你能从下面这张图片看出玄机： 多态帮助我们创建更多通用的接口。 用代码解释 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/polymorphism-in-java-tutorial-with-object-oriented-programming-example-code/</link>
                <guid isPermaLink="false">5fec32f039641a0517d5288c</guid>
                
                    <category>
                        <![CDATA[ Java ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 后端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Wed, 30 Dec 2020 08:01:33 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/banner-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>多态（Polymorphism）的字面意思是“<em>多种形式</em>”，它允许对象被视为可替代的。当你想让不同类型的对象完成相同的动作时，多态能帮你减少重复代码。</p>
<p>让我们解释一下这到底是什么意思。</p>
<h2 id="">用类比解释多态</h2>
<p>如果你有过国际旅行的经历，你的行李检查单上很可能会有电插头适配器。否则，你可能就无法给你的手机和其它设备充电了。</p>
<figure>
    <img src="https://www.freecodecamp.org/news/content/images/2020/10/call-me-fred-nBfTARHPxiU-unsplash-1-.jpg" alt="packing.jpg" width="600" height="400" loading="lazy">
    <figcaption style="text-align: center">
        <a href="https://unsplash.com/@callmefred?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Call Me Fred</a> 拍摄
    </figcaption>
</figure>
<p>很奇怪，世界上大概有 16 种不同类型的电源插座：两脚的、三脚的、圆形的、方形的，插脚的配置也五花八门。</p>
<p>大多数人的对策就是买一个通用的插头适配器。</p>
<p>换一种方式看待这个问题，普遍问题是我们的插座接口只接受一种类型的插头对象！插座不是多态的。</p>
<p>要是我们的插座能接受很多不同类型的插头，生活就没那么累了。我们可以通过创造不同形状的开口，让插座变成多态的，你能从下面这张图片看出玄机：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/socket-metaphor.svg" alt="socket-metaphor" width="600" height="400" loading="lazy"></p>
<p>多态帮助我们创建更多通用的接口。</p>
<h2 id="">用代码解释</h2>
<p>任何具有 IS-A 关系的对象都可以被认为是多态的，获得 IS-A 关系的途径有两种：通过继承（在类的签名上使用 <em>extends</em> 关键字）或通过接口（在类的签名上使用 <em>implements</em> 关键字）。</p>
<p>为了完全理解多态，你应该同时理解继承和接口。</p>
<pre><code class="language-java">class Dog extends Animal implements Canine{
 // ... some code here
}

</code></pre>
<p>根据上面这段代码，<code>Dog</code> 具有以下 IS-A 关系：<code>Animal</code>、<code>Canine</code> 和 <code>Object</code>（每个类都隐式继承 <a href="https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html">Object 类</a>，这听起来有些离谱！）。</p>
<p>让我们用一个简单的（傻瓜式的）例子来说明如何使用多态简化代码。我们想创建一个应用，应用中的审讯官能让任何动物开口说话。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/interrogation-1.png" alt="interrogation" width="600" height="400" loading="lazy"></p>
<p>我们将创建一个负责让动物开口说话的 <code>Interrogator</code> 类，但我们并不想为每种动物都写一个说话的方法：<code>convinceDogToTalk(Dog dog)</code>、 <code>convinceCatToTalk(Cat cat)</code>，等等。</p>
<p>我们更喜欢用一个可以接受任何动物的通用方法，怎么做呢？</p>
<pre><code class="language-java">class Interrogator{
    public static void convinceToTalk(Animal subject) {
        subject.talk();
    }
}

// 我们不想让任何人创建动物对象！
abstract class Animal {
    public abstract void talk();
}

class Dog extends Animal {
    public void talk() {
        System.out.println("Woof!");
    }
}

class Cat extends Animal {
    public void talk() {
        System.out.println("Meow!");
    }
}

public class App {
    public static void main(String[] args){
        Dog dog = new Dog();
        Cat cat = new Cat();
        Animal animal = new Dog();
        
        Interrogator.convinceToTalk(dog); //prints "Woof!"
        Interrogator.convinceToTalk(cat); //prints "Meow!"
        Interrogator.convinceToTalk(animal); //prints "Woof!"
    }
}
</code></pre>
<p>我们创建了一个 <code>convinceToTalk</code> 方法，它接收一个 <code>Animal</code> 类型的对象作为参数。在方法内部，我们会调用该对象的 <code>talk</code> 方法，只要这个对象的类型是 <code>Animal</code> 或其子类，编译器就会欣然接受。</p>
<p>Java 虚拟机（JVM）在运行时会根据对象所属的类决定调用哪个方法。如果这个对象的类型是 <code>Dog</code>，JVM 就会调用说“Woof”的方法实现。</p>
<p>这么做有两个好处：</p>
<ol>
<li>我们只需要写一个通用的方法，不需要做任何的类型检测。</li>
<li>如果我们在将来创建了一个新的动物类型，我们不需要修改 <code>Interrogator</code> 类。</li>
</ol>
<p>这种类型的多态被称为重写（overriding）。</p>
<h2 id="">重写</h2>
<p>我们之前讨论的例子是广义的重写，让我们给一个正式的定义和更多的细节吧。</p>
<p>当你在一个相关类中创建 <strong>同一实例方法</strong>（方法签名相同） 的不同实现时，重写就发生了。</p>
<p>在运行时，程序才会选择 <strong>对象类型</strong> 的方法。这就是重写也被成为运行时多态的原因。</p>
<p>重写的一种实现方式是：父类定义方法，而子类提供方法的不同实现。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/overriding-inheritance.png" alt="overriding inheritance" width="600" height="400" loading="lazy"></p>
<p>另一种实现重写的方式是：为接口中定义的方法提供不同的实现。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/overriding-interface.png" alt="overriding interface" width="600" height="400" loading="lazy"></p>
<p>重写方法的规则：</p>
<ol>
<li>方法本身必须是通过 IS-A 关系（通过 <code>extends</code> 或 <code>implements</code>）定义的。有时候你会发现这种方法被称为子类型多态（subtype polymorphism），这就是原因。</li>
<li>重写方法必须与原方法具有相同的参数列表。</li>
<li>重写方法的返回类型要么与原方法的返回类型相同，要么为原方法返回类型的子类。</li>
<li>重写方法的访问限制修饰符不能比原方法的更严格。</li>
<li>重写方法的访问限制修饰符可以比原方法的宽松。</li>
<li>重写方法 <em>不能</em> 抛出一个新的或者范围更广的受检异常。</li>
<li>重写方法可以抛出范围更小的或更少的受检异常（也可以不抛出）。例如声明了 <em>IOException</em> 的方法可以被声明了 <em>FileNotFoundException</em> 的方法重写，因为 <em>FileNotFoundException</em> 是 <em>IOException</em> 的子类。</li>
<li>重写方法可以抛出任何非受检异常，而不用管原方法是否有声明这些非受检异常。</li>
</ol>
<blockquote>
<p>推荐在重写方法的时候使用 <em>@Override</em> 注解，它能在编译时提供方法签名上的错误检查，能避免你违背上面的重写规则。</p>
</blockquote>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/override-annotation.png" alt="override annotation" width="600" height="400" loading="lazy"></p>
<h3 id="">禁止重写</h3>
<p>如果你不想某个方法被重写，把它声明为 final 即可：</p>
<pre><code class="language-java">class Account {
    public final void withdraw(double amount) {
        double newBalance = balance - amount;
        
        if(newBalance &gt; 0){
        	balance = newBalance;
        }
    }
}
</code></pre>
<h3 id="">静态方法</h3>
<p><strong>静态方法不可重写</strong>。你其实是在一个相关类中创建了那个方法的 <em>不同</em> 定义。</p>
<pre><code class="language-java">class A {
    public static void print() {
        System.out.println("in A");
    }
}

class B extends A {
    public static void print() {
        System.out.println("in B");
    }
}

</code></pre>
<p>运行示例中的 <code>Test</code> 类会打印出“in A”，说明这里并没有出现重写。</p>
<p>如果你把类 <code>A</code> 和 类 <code>B</code> 中的 <code>print</code> 方法变成实例方法（从方法的签名上移除 <code>static</code>），再次运行 <code>Test</code>类，打印出来的就是“in B”！重写正在发生。</p>
<p><strong>记住：重写通过对象类型选择方法，而不是通过变量类型。</strong>  🧐</p>
<h2 id="">重载（函数多态）</h2>
<p>重载（overloading）发生在你创建同一个方法的不同版本的时候。</p>
<p>方法名字必须相同，但是我们可以改变参数和返回值的类型。</p>
<p>你能在 Java 的 <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html">Math 类</a> 中找到很多方法重载的例子。<code>max</code> 方法被不同类型重载，在所有的情况中，它返回两个值中的最大值，但是它适用于不同的（不相关的）数值类型。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/overloading-max-example.png" alt="overloading-max-example" width="600" height="400" loading="lazy"></p>
<p>重载方法的选取由（引用）变量类型决定，重载在编译时完成。</p>
<p>重载方法为类的使用者提供了更好的灵活性。在使用者的应用中，可能存在多种不同格式的数据，也可能在不同情形下有不同的可用数据。</p>
<p>例如，<a href="https://docs.oracle.com/javase/8/docs/api/java/util/List.html">List</a> 类重载了 <code>remove</code> 方法。因为列表（List）是对象的有序集合，所以你可能想某个特定位置（下标）的对象从列表中移除。或者你可能并不知道具体的位置，不管那个对象在哪里，你只是想把它从列表中移除。这就是为什么会有两个版本的 <code>remove</code> 方法。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/list-overloaded-methods.png" alt="list-overloaded-methods" width="600" height="400" loading="lazy"></p>
<p>构造函数函数也可以被重载。</p>
<p>例如，<a href="https://docs.oracle.com/javase/8/docs/api/java/util/Scanner.html">Scanner</a> 类有很多不同类型的输入，这些输入都可以用来创建对象。下面是这个类的一个小快照。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/constructor.png" alt="constructor" width="600" height="400" loading="lazy"></p>
<p>重载方法的规则：</p>
<ol>
<li>它必须有一个不同的参数列表。</li>
<li>它可能有不同的返回类型。</li>
<li>它可能有不同的访问修饰符。</li>
<li>它可能抛出不同的异常。</li>
<li>父类的方法可以被子类重载。</li>
</ol>
<h2 id="">重写与重载的区别</h2>
<ol>
<li>重写必须基于 IS-A 关系的方法，重载则不必这样。重载可以在同一个类中出现。</li>
<li>重写方法是根据对象类型进行选取的，而重载方法的选取是根据（引用）变量类型的。</li>
<li>重写发生在运行时，而重载发生在编译时。</li>
</ol>
<h2 id="">参数多态</h2>
<p>Java 中的参数多态（parameteric polymorphism）是通过<a href="https://docs.oracle.com/javase/tutorial/extra/generics/index.html">泛型</a>实现的。</p>
<p>Java 5.0 中引入了泛型，它的设计初衷是扩展 Java 的类型系统，从而允许“一个类型或方法操作多种类型的对象，同时提供编译时的类型安全”。</p>
<p>基本上，泛型类或泛型方法中的泛型类型可以被替换为所有的类型。</p>
<p><a href="https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html">ArrayList</a> 就是一个简单的例子，它的类定义中有一个标志为 <code>&lt;E&gt;</code> 的泛型参数。一些像 <code>add</code> 这样的实例方法就在它们的签名中使用了这个泛型类型。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/arraylist-definition-2.png" alt="arraylist class definition" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/arraylist-definition-methods.png" alt="arraylist definition add methods" width="600" height="400" loading="lazy"></p>
<p>在创建 <code>ArrayList</code> 对象的时候，我们在尖括号中提供类型，以此填充整个类中定义的泛型引用。因此，如果我们创建了一个泛型类型为 <code>Dog</code> 的 <code>ArrayList</code>，<code>add</code> 方法将只会接受 <code>Dog</code> 对象作为参数。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/arraylist-dog-method-signature-1.png" alt="arraylist dog method signature" width="600" height="400" loading="lazy"></p>
<p>如果你尝试添加除 <code>Dog</code> 以外的任何东西，就会出现编译错误！如果你使用像 IntelliJ 这样的代码编辑器，你就能得到红色的波浪线，它会突出你犯的错误。如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/arraylist-type-checking-1.png" alt="arraylist type checking" width="600" height="400" loading="lazy"></p>
<h2 id="">结语</h2>
<p>多态是一个棘手的话题，对于编程小白来说更是如此。找出在代码中使用泛型的正确场合，是要花些时间的。</p>
<p>但是，一旦你习惯了它，你会发现多态能大幅改善你的代码。</p>
<h2 id="">照片归属</h2>
<p>标题的照片由来自 Unsplash 的 <a href="https://unsplash.com/@markusspiske?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Markus Spiske</a> 拍摄。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/polymorphism-in-java-tutorial-with-object-oriented-programming-example-code/">Polymorphism in Java Tutorial – With Object Oriented Programming Example Code</a>，作者：<a href="https://www.freecodecamp.org/news/author/rob/">Rob O'Leary</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Git 内部原理图解——对象、分支以及如何从零开始建仓库 ]]>
                </title>
                <description>
                    <![CDATA[ 我们中的许多人每天都在使用 git，但是有多少人知道它的内部是怎么运作的呢？ 例如我们使用 git commit  时发生了什么？提交（commit）与提交之间保存的是什么？两次提交之间难道只是文件的差异（diff）吗？如果是，这个差异是如何编码的？还是说每次提交都会保存一个当前仓库的完整快照（snapshot）呢？我们使用  git init 时到底发生了什么？ 很多 git 的使用者都不知道这几个问题的答案，但这又有什么关系呢？ 首先，作为专业人员，我们应当努力弄清楚手中使用的工具，尤其是那些我们一直都在使用的——比如 git。 但是我深刻地意识到，理解 Git 的工作原理在很多情况下都非常有用——不管是解决合并冲突、进行有趣的变基（rebase）操作，还是在某些东西变得有点不对劲的时候。 如果你有足够的 git 经验，对 git pull、git push、git add 或 git commit 这些命令得心应手，你会从本文中获益。 不过，为了确保我们在 git 的原理（尤其是本文上下所使用的术语）上步调一致，我们将从概览开始。 我也在 YouTube 上传了一个 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/git-internals-objects-branches-create-repo/</link>
                <guid isPermaLink="false">5fe8941a39641a0517d52674</guid>
                
                    <category>
                        <![CDATA[ Git ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Sat, 26 Dec 2020 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/photo-1501084291732-13b1ba8f0ebc-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我们中的许多人每天都在使用 <code>git</code>，但是有多少人知道它的内部是怎么运作的呢？</p><p>例如我们使用 <code>git commit</code> 时发生了什么？提交（commit）与提交之间保存的是什么？两次提交之间难道只是文件的差异（diff）吗？如果是，这个差异是如何编码的？还是说每次提交都会保存一个当前仓库的完整快照（snapshot）呢？我们使用 <code>git init</code> 时到底发生了什么？</p><p>很多 <code>git</code> 的使用者都不知道这几个问题的答案，但这又有什么关系呢？</p><p>首先，作为专业人员，我们应当努力弄清楚手中使用的工具，尤其是那些我们一直都在使用的——比如 <code>git</code>。</p><p>但是我深刻地意识到，理解 Git 的工作原理在很多情况下都非常有用——不管是解决合并冲突、进行有趣的变基（rebase）操作，还是在某些东西变得有点不对劲的时候。</p><p>如果你有足够的 <code>git</code> 经验，对 <code>git pull</code>、<code>git push</code>、<code>git add</code> 或 <code>git commit</code> 这些命令得心应手，你会从本文中获益。</p><p>不过，为了确保我们在 <code>git</code> 的原理（尤其是本文上下所使用的术语）上步调一致，我们将从概览开始。</p><p>我也在 YouTube 上传了一个涵盖本文所有内容的系列视频——欢迎<a href="https://www.youtube.com/playlist?list=PL9lx0DXCC4BNUby5H58y6s2TQVLadV8v7" rel="nofollow">在此</a>观看。</p><h1 id="-">本教程的内容</h1><p>我们将对日常使用的 <code>git</code> 的内部运行原理有一个比较深入的理解。</p><p>我们会从对象（object）——<strong>blob</strong>、<strong>树对象（tree）</strong> 和 <strong>提交对象（commit）</strong> 开始，然后简单讨论一下 <strong>分支（branch）</strong> 及其实现方式，之后会深入 <strong>工作目录（working directory）</strong>、<strong>暂存区（staging area）</strong> 和 <strong>仓库（repository）</strong>。</p><p>我们会确保理解了这些术语是与我们用来创建新仓库的那些命令之间是如何关联的。</p><p>接下来，我们会从零开始创建一个仓库——不使用 <code>git init</code>、<code>git add</code> 或 <code>git commit</code>。这会在我们使用 <code>git</code> 的过程中，<strong>加深我们对其内部正在发生的事情的理解</strong>。</p><p>我们也会创建新的分支、在分支间切换，再进行一些提交——全程不使用 <code>git branch</code> 或 <code>git checkout</code>。</p><p>在本文结束之前，<strong>你会觉得自己真的 <em>理解了</em> <code>git</code></strong>。你准备好了吗？😎</p><blockquote>译者注：建议读者配合 <a href="https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E5%BA%95%E5%B1%82%E5%91%BD%E4%BB%A4%E4%B8%8E%E4%B8%8A%E5%B1%82%E5%91%BD%E4%BB%A4" rel="nofollow">Git 内部原理</a>阅读本文。</blockquote><h1 id="git-blob-tree-commit">Git 对象——blob、tree 和 commit</h1><blockquote>译者注：译文中的 <strong>数据对象</strong>、<strong>树对象</strong> 和 <strong>提交对象</strong> 指的就是 blob、tree 和 commit 这三者。因为 Git 官网的文档<a href="https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%AF%B9%E8%B1%A1" rel="nofollow">Git 内部原理 - Git 对象</a>对三者进行了这样的翻译，本文是为了与其保持一致。但由于 blob 一词的特殊性，译文会直接保留原词，而不是将其翻译为“数据对象”。</blockquote><p>将 <code>git</code> 看成一个文件系统（尤其是该系统的实时快照）是很有用的。</p><p>一个文件系统从 <em>根目录（root directory）</em> 开始（在基于 UNIX 的系统中是 <code>/</code>），通常也会包含其它的目录（例如 <code>/usr</code> 或 <code>/bin</code>）。这些目录会包含其它的目录和（或）文件（例如 <code>/usr/1.txt</code>）。</p><p>在 <code>git</code> 中，文件的内容存储在一些被称为 <strong>blob</strong> （二进制大对象）的对象中。</p><p><strong>blob</strong> 与文件的不同在于，文件还会包含元数据（meta-data）。例如一个文件会“记住”它的创建时间，如果你把它移动到另一个目录，它的创建时间是不会改变的。</p><p>相反，<strong>blob</strong> 只是内容——数据的二进制流。除了内容以外，<strong>blob</strong> 不会记录它的创建时间、名字或任何其它东西。</p><p><code>git</code> 中的 <strong>blob</strong> 通过 <a href="https://en.wikipedia.org/wiki/SHA-1" rel="nofollow">SHA-1 哈希值</a> 唯一标识。SHA-1 哈希值由 20 个字节（byte）组成，通常表示成 40 个十六进制形式的字符。在这篇文章中，我们有时只会展示这个哈希值的前几个字符。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/e0180213af0d75b24faafcea0265b7fa14c90341f4d17044d6f695fc2cda21a1/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33342e706e67" class="kg-image" alt="Blob 有对应的 SHA-1 哈希值" width="600" height="400" loading="lazy"></figure><p>在 <code>git</code> 中，<strong>树对象（tree）</strong> 相当于目录。一个 <strong>树对象</strong> 基本上就是一个目录列表，它引用着 <strong>blob</strong> 和其它的 <strong>树对象</strong>。</p><p><strong>树对象</strong> 也用 SHA-1 哈希值唯一标识，它通过其它对象（<strong>blob</strong> 或 <strong>树对象</strong>）的 SHA-1 哈希值引用它们。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/cc836ce5be699267deee2d1dfdada0040e4068e24a59b824cad1884ecef78a8a/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33352e706e67" class="kg-image" alt="树对象是一个目录列表" width="600" height="400" loading="lazy"></figure><p>注意 <strong>CAFE7</strong> 这个 <strong>树对象</strong> 指向了 <strong>blob F92A0</strong>（<em>pic.png</em>），在另一个 <strong>树对象</strong> 中，同一个 <strong>blob</strong> 可能会有不同的名字。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/22e34c5f938f535779437e01bbe15ca87ab3ea0addb1650f3c24fa9d856d2d16/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33362e706e67" class="kg-image" alt="树对象可能包含子树对象和其它 blob" width="600" height="400" loading="lazy"></figure><p>上面这张图相当于一个文件系统，这个文件系统有一个根目录，根目录下有一个位于 <code>/test.js</code> 的文件和一个名为 <code>/docs</code> 的目录，<code>/docs</code> 目录下有两个文件：<code>/docs/pic.png</code> 和 <code>/docs/1.txt</code>。</p><p>现在是时候捕获该文件系统的一个快照了，把那个时刻存在的所有文件连同它们的内容保存下来。</p><p>在 <code>git</code> 中，一个快照就是一个 <strong>提交（commit）</strong>。一个 <strong>提交</strong> 对象包括一个指向主要 <strong>树对象</strong>（根目录）的指针和一些像 <strong>提交者</strong>、<strong>提交信息</strong> 和 <strong>提交时间</strong> 这样的元数据。</p><p>在大多数情况下，一个 <strong>提交</strong> 还会有一个或多个父 <strong>提交</strong>——之前的快照。当然，<strong>提交</strong> 对象也通过它们的 SHA-1 哈希值唯一标识。这些哈希值就是我们使用 <code>git log</code> 命令时看到的那些哈希值。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/293d2604d198c04ee6124c5342ae6225f3614ee24e509b372fcdc25adf1159d5/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33372e706e67" class="kg-image" alt="提交对象是某个时刻的快照。它引用着树的根节点。由于这是第一次提交，它没有父节点" width="600" height="400" loading="lazy"></figure><p>每个 <strong>提交</strong> 都持有 <em>完整的快照</em>，并不只是与之前 <strong>提交</strong> 之前的差异。</p><p>那么它是怎么工作的呢？难道它不代表我们每次提交都必须保存很多数据吗？</p><p>让我们来看看改变一个文件的内容会发生什么。我们编辑 <code>1.txt</code>，加一个感叹号——也就是把文件的内容由 <code>HELLO WORLD</code> 变为 <code>HELLO WORLD!</code>。</p><p>这个改变意味着我们会有一个新的 <strong>blob</strong>，它有新的 SHA-1 哈希值。这是有意义的，因为 <code>sha1("HELLO WORLD")</code> 与 <code>sha1("HELLO WORLD!")</code> 并不相同。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/803ffa1082a137a3314e90c042c4dcbd5c3abde1b8f3e69aaacdeeba822164a6/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33382e706e67" class="kg-image" alt="改变 blob 会得到新的 SHA-1 值" width="600" height="400" loading="lazy"></figure><p>由于我们得到了一个新的哈希值，所以对应 <strong>树对象</strong> 的目录也会改变。毕竟，我们的 <strong>树对象</strong> 不再指向 <strong>blob 73D8A</strong> 了，而是指向了 <strong>blob 62E7A</strong>。当我们改变 <strong>树对象</strong> 的内容时，我们也改变了它的哈希值。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/b80e5342dd3871c8e8db1af02160ff7ba1a509d8d3f1e5b30e951f1530709dc0/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d33392e706e67" width="600" height="400" loading="lazy"><figcaption>blob 变了，指向它的树对象也需要变</figcaption></figure><p>现在，由于原来那个 <strong>树对象</strong> 的哈希值已经不同了，我们也需要改变它的 <strong>父树对象</strong>——后者不再指向 <strong>tree CAFE7</strong>了，而是指向了 <strong>tree 246001</strong>。最终，<strong>父树对象</strong> 也会有一个新的哈希值。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/43ae3b3c1d2930600f6e1eb8ce0a175700bb23f1a2941351b42dbb63f6f41a3f/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34302e706e67" class="kg-image" alt="根节点也变了，它的哈希值也变了" width="600" height="400" loading="lazy"></figure><p>几乎做好创建一个新 <strong>提交</strong> 对象的准备了，我们好像会再一次保存很多的数据——整个文件系统。但是真的有必要这么做吗？</p><p>实际上，一些对象（尤其是 <strong>blob</strong> 对象）相比起之前的提交来说没有任何改变——<strong>blob F92A0</strong>仍然原封不动，<strong>blob F00D1</strong> 也一样。</p><p>这就是其中的秘诀——只有对象改变了，我们才再次保存它。在这个例子中，我们不需要再次保存 <strong>blob F92A0</strong> 和 <strong>blob F00b1</strong>。我们只需要通过它们的哈希值引用它们，然后我们可以创建 <strong>提交</strong> 对象。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/e586eff3853acfe86cf448c8d0aeff9f5d6f4a3a6ae4f5b180a6ed01f381b61e/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34312e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34312e706e67" width="600" height="400" loading="lazy"><figcaption>那些没有丝毫变化的 blob 是通过它们的哈希值被引用的</figcaption></figure><p>由于这次 <strong>提交</strong> 不是第一次 <strong>提交</strong>，所以它有一个父节点——<strong>commit A1337</strong>。</p><h4 id="-git-">回顾一下，我们介绍了三种 git 对象：</h4><ul><li><strong>blob</strong>——文件的内容。</li><li><strong>树对象</strong>——一个（由 <strong>blob</strong> 和 <strong>树对象</strong> 组成的）目录列表。</li><li><strong>提交对象</strong>——工作树的一个快照。</li></ul><p>让我们思考一下这些对象的哈希值吧。如果我写了 <code>git is awesome!</code> 并从它创建了一个 <strong>blob</strong>。你也在自己的系统上这么做，我们会有相同的哈希值吗？</p><p>答案是肯定的。因为这两个 <strong>blob</strong> 有相同的内容，自然也会有相同的 SHA-1 哈希值。</p><p>如果我创建了一个引用 <code>git is awesome!</code> 这个 <strong>blob</strong> 的 <strong>树对象</strong> ，赋给它一个特定的名字和元数据，你也在自己的系统上重复我的操作。我们会有相同的哈希值吗？</p><p>答案还是肯定的。因为这两个 <strong>树对象</strong> 是相同的，它们会有同样的哈希值。</p><p>如果我创建了一个指向那个 <strong>树对象</strong> 的 <strong>提交对象</strong>，提交信息为 <code>Hello</code>，你也在自己的系统上重复了一遍这个操作，结果会怎样呢？我们的哈希值还会相同吗？</p><p>这个时候的答案是否定的。即使我们的 <strong>提交对象</strong> 指向了相同的 <strong>树对象</strong>，它们也会有不同的 <strong>提交详情</strong>——时间、提交者，等等。</p><h1 id="git-">Git 中的分支</h1><p><strong>分支（branch）只不过是提交对象的命名引用</strong>。</p><blockquote>译者注：分支引用的是 <strong>提交对象</strong>，为了简单起见，下文在谈分支时，有时候会将 <strong>提交对象</strong> 简称为 <strong>提交</strong>。</blockquote><p>我们可以一直用 SHA-1 哈希值引用一个 <strong>提交</strong>，但是人们通常喜欢以其他形式命名对象。<strong>分支</strong> 恰好是引用 <strong>提交</strong> 的一种方式，实际上也只是这样。</p><p>在大多数仓库中，主线开发都是在一个叫做 <code>master</code> 的分支上完成的。<code>master</code> 只是一个名字，它是在我们使用 <code>git init</code> 命令的时候被创建的。正因为如此，它被广泛使用。然而，它并不特别，我们可以用任何我们喜欢的名字代替它。</p><p>通常，分支指向的是当前开发线上的最近一次 <strong>提交</strong>。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/efa001a19d15c01a1eced1ceb1d581b9eb5f20f78a726fa922d1b125c0a9955a/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34322e706e67" class="kg-image" alt="分支只不过提交的命名引用" width="600" height="400" loading="lazy"></figure><p>我们通常使用 <code>git branch</code> 命令创建一个新分支，而我们实际创建的却是另一个指针（pointer）。假设我们使用 <code>git branch test</code> 命令创建了一个名为 <code>test</code> 的分支，我们实际上是创建了另一个指针，它指向当前分支上的同一 <strong>提交</strong>。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/cd47885461841a143014b3eef8697e32d9de2eb97e26bd56b22db7d49103e43d/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34332e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34332e706e67" width="600" height="400" loading="lazy"><figcaption>使用 <code>git branch</code> 创建另一个指针</figcaption></figure><p><code>git</code> 是怎么知道我们当前所在的分支呢？答案是它维护了一个名为 <code>HEAD</code> 的特殊指针。通常情况下，<code>HEAD</code> 会指向一个分支，这个分支指向一个 <strong>提交</strong>。有时候，<code>HEAD</code> 也能直接指向一个 <strong>提交</strong>，不过这不是我们的重点。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/f0dcc32c65098ca48c7974af49466103d2e49622110a4097021dcfcbd4b9ce97/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34342e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34342e706e67" width="600" height="400" loading="lazy"><figcaption>HEAD 指向我们当前所在的分支</figcaption></figure><blockquote>译者注：活动分支（active branch）指的是我们当前所在的分支，也就是 <code>HEAD</code> 指向的分支。</blockquote><p>要将活动分支切换到 <code>test</code>，我们可以使用命令 <code>git checkout test</code>。现在我们已经能猜到这条命令真正做的事情了——它只不过是把 <code>HEAD</code> 指向的分支改成了 <code>test</code>。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/5c0cda513d9f2961c3d2e6f7c0b5b9294f4db8fe93a02c24264722ea0f2e5456/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34352e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34352e706e67" width="600" height="400" loading="lazy"><figcaption><code>git checkout test</code> 改变 <code>HEAD</code> 指向的分支</figcaption></figure><p>在创建 <code>test</code> 分支之前，我们也可以使用 <code>git checkout -b test</code>，这条命令等价于先运行 <code>git branch test</code> 创建分支，再运行 <code>git checkout test</code> 使 <code>HEAD</code> 指向新的分支。</p><p>如果我们做了一些改动并使用 <code>git commit</code> 创建了一个新 <strong>提交</strong> 呢？这个新 <strong>提交</strong> 会被添加到哪个分支上呢？</p><p>答案是 <code>test</code> 分支，因为它是当前的活动分支（因为 <code>HEAD</code> 指向了它）。之后，<code>test</code> 指针会移动至新添加的 <strong>提交</strong> 上。注意 <code>HEAD</code> 仍然指向 <code>test</code>。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/716a0062270c2a698d47f30f5f75a0eb7eeaf0acd4f48a25101a41cf4a2b72d8/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34362e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34362e706e67" width="600" height="400" loading="lazy"><figcaption>每次执行 <code>git commit</code> 命令都会让分支的指针移动到新创建的提交上</figcaption></figure><p>因此，如果我们使用 <code>git checkout master</code> 回到 master 分支，我们就让 <code>HEAD</code> 的再次指向 <code>master</code> 了。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/a1eddf190ebc79fd46bd15e6fb9d527e8570eedce43c1f3c6db5f7c61664bb9b/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34372e706e67" width="600" height="400" loading="lazy"></figure><p>如果我们现在创建一个新的 <strong>提交</strong>，它就会被添加到 <code>master</code> 分支，<strong>commit B2424</strong> 会成为新提交的父节点。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/68a6eb53b0b21ea4bef6189652d31ec9deaca379d4620010050d102795243522/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34382e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34382e706e67" width="600" height="400" loading="lazy"></figure><h1 id="-git--1">如何在 Git 中记录变化</h1><p>通常，我们在 <strong>工作目录（working dir）</strong> 中编写源代码。<strong>工作目录</strong> （或 <strong>工作树（working tree）</strong>）可以是文件系统上的任何一个目录，它关联着一个 <strong>仓库（repository）</strong> 。目录内不仅包含工程的文件夹和文件，还包含一个名为 <code>.git</code> 的目录。稍后我们会再讨论 <code>git</code> 这个目录。</p><p>在做了一些改动之后，我们想把这些改动记录到我们的 <strong>仓库</strong> 中。一个 <strong>仓库</strong> （缩写：<strong>repo</strong>）就是一系列 <strong>提交</strong> 的集合，每个 <strong>提交</strong> 都是工程 <strong>工作树</strong> 的归档。除了我们自己机器上的提交外，仓库也会包含他人机器上的提交。</p><p><strong>仓库</strong> 也包含除代码文件以外的其它东西，例如 <code>HEAD</code> 指针、分支等等。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/d6ac18d941ed9016c28da6ccfe8d38e286cc883186d3fec2ced29716fc77cd35/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d34392e706e67" width="600" height="400" loading="lazy"></figure><p>你可能使用过的其它和 <code>git</code> 类似工具，但是 <code>git</code> 并不会像其它工具那样直接将变化从 <strong>工作树</strong> 提交到 <strong>仓库</strong>。相反，它会先把这些变化注册到一个被称为 <strong>索引（index）</strong> 或 <strong>暂存区（staging area）</strong> 的地方。</p><p>这两个术语指的都是同一个东西，它们也经常被 <code>git</code> 的文档使用，我们将会在这篇文章中交替使用它们。</p><p>当我们 <code>checkout</code> 到一个分支时，<code>git</code> 会将上一次检出到工作目录中的所有文件填充到 <strong>索引</strong>，它们看起来就像最初被检出时的样子。之后执行 <code>git commit</code> 时， <strong>提交</strong> 会在当前 <strong>索引</strong> 的基础上创建。</p><p><strong>索引</strong> 允许我们精心准备每次 <strong>提交</strong>。举个例子，自上一次 <strong>提交</strong> 以来，我们的 <strong>工作目录</strong> 中可能有两个文件发生了变化，但是我们可能只想将其中的一个添加到 <strong>索引</strong>（使用 <code>git add</code>），然后使用 <code>git commit</code> 记录这一个文件的变化。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/f1b0275e1a35e382be6a13b3a4f5573b326d902b5762f5dc789149718666508e/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35302e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35302e706e67" width="600" height="400" loading="lazy"></figure><p><strong>工作目录</strong> 下文件的状态不外乎有两种：<strong>已跟踪（tracked）</strong> 或 <strong>未跟踪（untracked）</strong>。</p><p><strong>已跟踪文件</strong> 是指那些 <code>git</code> 已经知道的文件。它们要么已经在上一次快照（<strong>提交</strong>）中，要么已经被 <strong>暂存（staged）</strong>（换句话说，它们已经在 <strong>暂存区</strong> 中）。</p><p><strong>工作目录</strong> 中除已跟踪文件以外的所有其它文件都属于 <strong>未跟踪文件（untracked）</strong>，它们既没有在上次快照（<strong>提交</strong>）中，也没有在 <strong>暂存区</strong> 中。</p><h1 id="--1">创建仓库的常规方式</h1><p>让我们确认下我们已经理解了“创建<strong>仓库</strong>”时介绍的相关术语。在我们更加深入这个过程之前，这只是一个非常高阶的视角。</p><p>注意：大多数带有 shell 命令的文章展示的都是 UNIX 命令。我将同时给出 Windows 和 UNIX 下的命令。为了换换花样，我会给出 Windows 下面的截图。当两种环境下的命令完全一样时，我只会给出一次命令。</p><p>我们用 <code>git init repo_1</code> 初始化一个新的 <strong>仓库</strong>，然后用 <code>cd repo_1</code> 切换到仓库所在目录。借助 <code>tree /f .git</code> 命令，我们可以看到运行 <code>git init</code> 之后 <code>.git</code> 目录下面出现了很多子目录（<code>/f</code> 表示在 <code>tree</code> 的输出中包含文件）。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/3220415b0dd6d139df9d641a0f281a7693b73b218e813a153cf6a486ab43f06c/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35312e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35312e706e67" width="600" height="400" loading="lazy"></figure><p>让我们在 <code>repo_1</code> 目录中创建一个文件吧：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/a330f5cd817380a7ffb7ba188561d08b5b63d3d8775877fb0d4f47a79101d032/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35322e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35322e706e67" width="600" height="400" loading="lazy"></figure><p>Linux 系统：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/4bb74770fb3779eeebb3f32f889a760c8f7844c0ab589695f88f1ed0275f8f88/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35332e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35332e706e67" width="600" height="400" loading="lazy"></figure><p>这个文件已经在我们的 <strong>工作目录</strong> 中了。目前，我们还没有将它添加到 <strong>暂存区</strong>，所以它是 <strong>未跟踪</strong> 状态。让我们用 <code>git status</code> 验证一下：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/b666ec923893c5813486e29d378185f127d52f4ba98b277990714c9b06d8cadc/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35342e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35342e706e67" width="600" height="400" loading="lazy"></figure><p>因为我们没有将新的文件添加到暂存区，所以它还是未跟踪状态，它也没有在之前的提交中</p><p>我们现在用 <code>git add new_file.txt</code> 将这个文件添加到 <strong>暂存区</strong>，再用 <code>git status</code> 验证一下它是否已经被暂存了：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/39732710b8f0cb1bc76170c2226f41c5c42ace65c6143428c9d3e64f2f5ffcbc/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35352e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35352e706e67" width="600" height="400" loading="lazy"><figcaption>添加新的文件到暂存区</figcaption></figure><p>我们可现在可以用 <code>git commit</code> 创建一个 <strong>提交</strong>：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/e94f9b18f572bb0ed8cd7a4726e800159e5e41a5fc8c123ec618d3fa5e65d59c/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35362e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35362e706e67" width="600" height="400" loading="lazy"></figure><p><code>.git</code> 目录有变化吗？我们用 <code>tree /f .git</code> 检查一下：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/753865f0e799e5ba6c65aae53addfb434687904ef76cc1e875334bf7288b8657/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d35372e706e67" width="600" height="400" loading="lazy"><figcaption><code>git</code> 目录中的很多东西已经变了</figcaption></figure><p>很明显，很多东西都变了。是时候深入 <code>.git</code> 的结构，理解执行 <code>git init</code>、<code>git add</code> 或 <code>git commit</code> 之后发生的什么事情了。</p><h1 id="--2">是时候上干货了</h1><p>目前我们已经讲了一些 Git 的基础知识，现在已经做好 <em>Git 上路</em> 的准备了。</p><p>为了深入理解 <code>git</code> 是如何工作的，我们将从零开始创建一个 <strong>仓库</strong>。</p><p>我们不会使用 <code>git init</code>、<code>git add</code> 或 <code>git commit</code>，这会让我们更好地理解这个过程。</p><h1 id="-git">如何设置 <code>.git</code></h1><p>先创建一个新目录，然后在里面运行 <code>git status</code>：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/f0c46031e68c1ba8177c25210b82e7900911fe80815bf6335657fa9a1afd975a/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130362e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130362e706e67" width="600" height="400" loading="lazy"></figure><p>好吧，因为我们没有 <code>.git</code> 文件夹，<code>git</code> 好像不怎么高兴。我们先把这个目录创建出来：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/2d3f6914bdb84144ebc7285841388991bd688534c5fb8cc9136d11a2477428ba/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130372e706e67" width="600" height="400" loading="lazy"></figure><p>很明显，只创建一个 <code>.git</code> 目录还不够。我们需要往这个目录添加一些东西。</p><p><strong>一个 git 仓库有两个主要组成部分：</strong></p><ol><li>一组对象——<strong>blob</strong>、<strong>树对象</strong> 和 <strong>提交对象</strong>。</li><li>一个命名这些对象的方式——称为 <strong>引用</strong>。</li></ol><blockquote>译者注：引用是 Git 中的一个重要概念，读者可以进一步阅读 <a href="https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%BC%95%E7%94%A8" rel="nofollow">Git 引用</a>。</blockquote><p>一个 <strong>仓库</strong> 可能还包含一些其它的东西，比如 git 钩子（hooks）。不过，仓库至少必须要有对象和引用。</p><p>让我们分别为对象和引用（简称：<strong>refs</strong>）各创建一个目录，Windows 下的两个目录分别为 <code>.git\objects</code> 和 <code>.git\refs</code>（UNIX 下的两个目录分别为 <code>.git/objects</code> 和 <code>.git/refs</code>）。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/2017908bbf9f87e80b0bec34ca6c78c4971beeb5d71c8e7aecca7ba1874f055a/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130382e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130382e706e67" width="600" height="400" loading="lazy"></figure><p><strong>分支</strong> 是引用的一种，<code>git</code> 内部将 <strong>分支</strong> 称为 <strong>heads</strong>，所以我们会为它们创建一个目录 <code>git\refs\heads</code>。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/3f2e4cdc3b6f59818a6e7300cdacabce7274010c60cac187d0db90b05864e7e4/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3130392e706e67" width="600" height="400" loading="lazy"></figure><p>然而 <code>git status</code> 的输出还是纹丝不动：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/baac8b0cbedc86d550edb38cf610c2275b1f21f103fc5d9fc44a9d508ba20729/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131302e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131302e706e67" width="600" height="400" loading="lazy"></figure><p>在寻找 <strong>仓库</strong> 中的 <strong>提交</strong> 时，<code>git</code> 怎么知道该从何开始呢？我之前解释过，它会寻找 <code>HEAD</code>，而 <code>HEAD</code> 指向着活动分支。</p><p>所以，我们需要创建 <code>HEAD</code>，它是一个位于 <code>.git\HEAD</code> 的文件。我们可以这么做：</p><p>Windows：<code>&gt; echo ref: refs/heads/master &gt; .git\HEAD</code></p><p>UNIX：<code>$ echo "ref: refs/heads/master" &gt; .git/HEAD</code></p><p>⭐ 所以我们现在知道 <code>HEAD</code> 是如何实现的了——它只是一个文件，文件内容描述了它所指向的分支。</p><p>执行上面的命令以后，<code>git status</code> 似乎改变它的主意了：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/41a9956e4f2ba1c9e91432c9de8c55d5589c358de7917f8aa23bc6ac45a30bc9/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131312e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131312e706e67" width="600" height="400" loading="lazy"><figcaption>HEAD 只不过是一个文件</figcaption></figure><p>注意：虽然我们还没有创建 <code>master</code> 分支，但是 <code>git</code> 相信我们就在这个分支上。之前有讲过，<code>master</code>只是一个名字。如果我们想的话，也可以让 <code>git</code> 认为我们在 <code>banana</code> 分支上：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/4d5335f0010eee02dc7615658a8203f48bac41a7ea66508f353862ad8d90f0dc/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131322e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131322e706e67" width="600" height="400" loading="lazy"></figure><p>🍌</p><p>按照惯例，我们将在本文的剩余部分中切回 <code>master</code> 分支。</p><p>我们已经准备好了 <code>git</code> 目录，现在继续往下，来一次 <strong>提交</strong>（同样地，不使用 <code>git add</code> 或 <code>git commit</code>）。</p><h1 id="git--1">Git 中的底层命令与上层命令</h1><p>这个时候，区分 <strong>底层（plumbing）</strong> 和 <strong>上层（porcelain）</strong> 两类 <code>git</code> 命令会对你很有帮助。这两个术语的应用奇怪地来自于马桶（没错，就是🚽）。马桶通常是用陶瓷（porcelain）做的，它的基本结构是管道（plumbing，上水道和下水道）。</p><p>我们可以说上层命令为底层命令提供了一个用户友好的接口。大多数人只会涉及到上层命令。然而，当事情变得（非常）糟糕时，有人可能就会想知道为什么，他们会卷起袖子去检查底层命令。（注意：这些术语并不是我发明的，它们在 <code>git</code> 中的使用非常广泛）。</p><blockquote>译者注：读者若想更好的理解这两个术语，建议阅读 <a href="https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E5%BA%95%E5%B1%82%E5%91%BD%E4%BB%A4%E4%B8%8E%E4%B8%8A%E5%B1%82%E5%91%BD%E4%BB%A4" rel="nofollow">Git 内部原理 - 底层命令与上层命令</a>。</blockquote><p><code>git</code> 使用这些术语进行类比，从而将用户不常使用的底层命令（plumbing）和那些更友好的高层（porcelain）命令区分开。</p><p>目前，我们已经接触过上层命令——<code>git init</code>、<code>git add</code> 和 <code>git commit</code>。接下来，我们转到底层命令。</p><h1 id="--3">如何创建对象</h1><p>让我们从创建对象并将其写入 <code>git</code> 的对象数据库开始吧，<code>git</code> 的对象数据库位于 <code>.git\objects</code> 中。第一条底层命令 <code>git hash-object</code> 会让我们将找到 <strong>blob 对象</strong> 的 SHA-1 哈希值。方式如下：</p><p>Windows：</p><p><code>&gt; echo git is awesome | git hash-object --stdin</code></p><p>UNIX：</p><p><code>$ echo "git is awesome" | git hash-object --stdin</code></p><p>我们使用 <code>--stdin</code> 告知 <code>git hash-object</code> 从标准输入（standard input）获取输入内容，这将给我们提供相应的哈希值。</p><p>为了真的将该 <strong>blob 对象</strong> 写入 <code>git</code> 的对象数据库，我们可以简单地给 <code>git hash-object</code> 加一个 <code>-w</code> 开关。然后，检查 <code>.git</code> 目录中的内容，看看它们有没有改变。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/cc090cc8b09a6ad48a4c1d89f291077a29105b68f42ed62e51896fcb37436754/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131332e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131332e706e67" width="600" height="400" loading="lazy"><figcaption>将一个 blob 对象写入对象数据库</figcaption></figure><p>我们现在可以看到，这个 <strong>blob</strong> 的哈希值为 <code>54f6...36</code>， <code>.git\objects</code> 下也多出来了一个名为 <code>54</code> 的目录，目录内有一个名为 <code>f6..36</code> 的文件。</p><p>所以，<code>git</code> 实际上是使用 SHA-1 哈希值的前两个字符作为目录的名字，剩余字符用作 <strong>blob</strong> 所在文件的文件名。</p><p>为什么要这样呢？考虑一个非常大的仓库，仓库的数据库内存有三十万个对象（<strong>blob 对象</strong>、<strong>树对象</strong> 和 <strong>提交对象</strong>）。从这三十万个哈希值中找出一个值会花些时间，因此，<code>git</code> 将这个问题划分成了 256 份。</p><p>为了查找上面的那个哈希值，<code>git</code> 会先寻找 <code>.git\objects</code> 目录下名为 <code>54</code> 的目录，然后搜索那个目录，这进一步缩小了搜索范围。<code>.git\objects</code> 目录下最多可能会有 256 个子目录（从 <code>00</code> 到 <code>FF</code>）。</p><p>回到生成 <strong>提交对象</strong> 的过程中来，现在我们已经创建了一个对象，它的类型是什么呢？我们可以通过另一个底层命令 <code>git cat-file -t</code> （<code>-t</code> 代表“type”）瞧一瞧：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/40b23162d472efe03bb0075b13bbfaf01a4512a945765f6d3013b9f9692b29fb/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131342e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131342e706e67" width="600" height="400" loading="lazy"></figure><p>不出所料，这个对象是一个 <strong>blob</strong>。我们还可以使用 <code>git cat-file -p</code> （<code>-p</code> 代表“pretty-print”）查看它的内容：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/29d6b3199302734b339b79ed9187b2f2c8c1eace1aa3b7a5bd0690988ed4e0b3/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131352e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131352e706e67" width="600" height="400" loading="lazy"></figure><p>创建 <strong>blob</strong> 这个过程通常发生在我们将一些东西添加到 <strong>暂存区</strong> 的时候——也就是我们使用 <code>git add</code> 的时候。</p><p>记住：<code>git</code> 是为 <em>整个</em> 暂存的文件创建 <strong>blob</strong>。即使文件中只有修改或添加了一个字符（如同我们在之前的例子红添加 <code>!</code> 一样），该文件也会有一个新的 <strong>blob</strong>，这个 <strong>blob</strong> 有着新的哈希值。</p><p><code>git status</code> 会有任何改变吗？</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/578fe0f620ccb2af2ec033b99a88e17f1e5f14e4df3bae31701260c500776c44/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131362e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131362e706e67" width="600" height="400" loading="lazy"></figure><p>显然没有。向 <code>git</code> 的内部数据库中添加一个 <strong>blob</strong> 对象并不会改变状态，因为 <code>git</code> 在这个阶段是不知道任何已跟踪或未跟踪文件的。</p><p>我们需要跟踪这个文件——把它添加到 <strong>暂存区</strong>。为此，我们可以使用底层命令 <code>git update-index</code>，例如：<code>git update-index --add --cacheinfo 100644 &lt;blob-hash&gt; &lt;filename&gt;</code>。</p><p>注意：<code>cacheinfo</code> 是一个<a href="https://github.com/git/git/blob/master/Documentation/technical/index-format.txt">git 存储的</a>十六位的文件模式，这个模式遵循 <a href="http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_stat.h.html" rel="nofollow">POSIX 类型和模式</a> 的布局。这超出了本文讨论的范围。</p><p>运行上述命令会改变 <code>.git</code> 目录的内容：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/ae084e692272bca730cc56eaea73d2ebad1e580dcc8fd5bf6428807083e318b3/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131372e706e67" width="600" height="400" loading="lazy"></figure><p>你能发现变化吗？多了一个名为 <code>index</code> 的新文件。这就是著名的 <strong>索引</strong> （或 <strong>暂存区</strong>），它基本上是一个位于 <code>.git\index</code> 中的文件。</p><p>既然 <strong>blob</strong> 已经被添加到了 <strong>索引</strong>，我们希望 <code>git status</code> 看起来会有所不同，像这样：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/1c1a83472a402d25e141826a2ceb21afc9b8f61e7a6690af4d56c4bb211f7ada/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131382e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131382e706e67" width="600" height="400" loading="lazy"></figure><p>真有趣！这里发生了两件事。</p><p>第一件事，我们可以在 <code>changes to be committed</code> 中看到绿色的 <code>new_file.txt</code>。这是因为 <strong>索引</strong> 中有了 <code>new_file.txt</code>，它正等着被提交。</p><p>第二件事，我们可以看到红色的 <code>new_file.txt</code>——因为 <code>git</code> 相信 <code>my_file.txt</code> 这个 <em>文件</em> 已经被删除了，并且它没有被暂存。</p><p>这发生在我们将内容为 <code>git is awesome</code> 的 <strong>blob</strong> 添加到对象数据库中的时候，我们告诉 <strong>索引</strong> ，那个 <strong>blob</strong> 的内容在文件 <code>my_file.txt</code> 中，但是我们从未创建过那个文件。</p><p>通过将那个 <strong>blob</strong> 的内容写入我们文件系统中名为 <code>my_file.txt</code> 的文件，我们可以很容易地解决这个问题：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/0d4a4af4b155c8955a5103cda67346ba00ccc797e7cbfb000812b0451de421f4/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3131392e706e67" width="600" height="400" loading="lazy"></figure><p>执行 <code>git status</code> 后，它将不再出现在红色内容中：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/c2b63869333bcee7e5604ca55d31007e8a5a5df9c30b1bb3397b9de7584f6fdf/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132302e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132302e706e67" width="600" height="400" loading="lazy"></figure><p>现在是时候从我们的 <strong>暂存区</strong> 创建一个 <strong>提交</strong> 对象了。如上所述，一个 <strong>提交</strong> 对象引用着一个 <strong>树对象</strong>，所以我们需要创建一个 <strong>树对象</strong>。</p><p>我们可以用 <code>git write-tree</code> 做这件事，它会在一个 <strong>树对象</strong> 中记录 <strong>索引</strong> 的内容。当然，我们可以使用 <code>git cat-file -t</code> 进行确认：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/fc03201152d03fcf4066801a76b1c7092984f931890faee5b197a604c6ba7686/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132312e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132312e706e67" width="600" height="400" loading="lazy"><figcaption>创建索引的树对象</figcaption></figure><p>我们还可以用 <code>git cat-file -p</code> 查看它的内容：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/23b16c7b9a43c6fe8e41e2fdab74ec1d69053253f3ba29581dadc29e04157d69/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132322e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132322e706e67" width="600" height="400" loading="lazy"></figure><p>太棒了！我们创建了一个 <strong>树对象</strong>，现在我们需要创建一个引用这个 <strong>树对象</strong> 的 <strong>提交</strong> 对象。为此，我们可以使用 <code>git commit-tree &lt;tree-hash&gt; -m &lt;commit message&gt;</code>：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/6956c5d036f89b9b4785b51a323f2b38c0fddc3f8d3ae5b1f6471e4d345fbbd4/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132332e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132332e706e67" width="600" height="400" loading="lazy"></figure><p>你现在应该对查看对象类型和打印对象内容的命令感到得心应手了：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/f0054e82ff5772b5d9041ad9176568448c9836cf537a8c51b23b3c490351b42b/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132342e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132342e706e67" width="600" height="400" loading="lazy"><figcaption>创建一个提交对象</figcaption></figure><p>注意这个 <strong>提交</strong> 并没有 <strong>父节点</strong>，因为它是第一个 <strong>提交</strong>。当我们添加另一个 <strong>提交</strong> 时，我们就得声明它的 <strong>父节点</strong>了——我们稍后会做这个。</p><p>我们刚得到的哈希值（<code>80e...8f</code>）是一个 <strong>提交对象</strong> 的哈希值。实际上我们非常习惯使用这些哈希值——我们一直都在看它们。注意这个 <strong>提交对象</strong> 拥有一个 <strong>树对象</strong>，树对象有自己的哈希值，不过我们几乎不会显式地指定这个哈希值。</p><p><code>git status</code> 会有所变化吗？</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/e670ec69452a4053587d4206ce70f78dece2dd0dbbbe2900716d42b1e73df059/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132352e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132352e706e67" width="600" height="400" loading="lazy"></figure><p>并没有。🤔</p><p>为什么呢？<code>git</code> 需要知道最近一次 <strong>提交</strong>，才能知道文件已经被提交。那么 <code>git</code> 是怎么做的呢？它会去找 <code>HEAD</code>：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/c19ac6c84cf427cc803394a3b991b9879ae4e340a63dd1a8d6c4bd2b183b4be5/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132362e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132362e706e67" width="600" height="400" loading="lazy"><figcaption>在 Windows 上查看 <code>HEAD</code></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/87a111da9944c7faebc0e81591bd91b4fdb3c8a70ff6c1be6e2cadd5fd5d733d/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132372e706e67" width="600" height="400" loading="lazy"><figcaption>在 UNIX 上查看 <code>HEAD</code></figcaption></figure><p><code>HEAD</code> 指向 <code>master</code>，但是 <code>master</code> 是什么呢？我们还没有创建它呢。</p><p>如同我们在前面解释的那样，分支只是 <strong>提交对象</strong> 的命名引用。这时，我们想要让 <code>master</code> 指向哈希值为 <code>80e8ed4fb0bfc3e7ba88ec417ecf2f6e6324998f</code> 的 <strong>提交对象</strong>。</p><p>这实现起来很简单，在 <code>\refs\heads\master</code> 创建一个文件，文件内容为这个哈希值。像这样：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/3de2646b0968021448ea4da5c8dbe05733f1fb003edd5071cf03ed2567ce8b26/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132382e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132382e706e67" width="600" height="400" loading="lazy"></figure><p>⭐ 总而言之，<strong>分支</strong> 只是 <code>.git\refs\heads</code> 中的一个文件，文件内容为该分支所指向的 <strong>提交对象</strong> 的哈希值。</p><p>现在，<code>git status</code> 和 <code>git log</code> 终于欣赏我们的付出了：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/2e37c264bdd32f3f744d5858c7b0ebef808f95fe6b0fd7087cb8966370209a83/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3132392e706e67" width="600" height="400" loading="lazy"></figure><p>我们已经成功创建出了一个 <strong>提交</strong>，全程没有使用上层命令！是不是很酷？🎉</p><h1 id="-git--2">与 Git 分支一起工作——背后的故事</h1><p>就像我们不借助 <code>git init</code>、<code>git add</code> 或 <code>git commit</code> 创建 <strong>仓库</strong> 和 <strong>提交</strong> 一样，我们将要创建 <strong>分支</strong>，在不同 <strong>分支</strong> 间来回切换，整个过程也不使用上层命令（<code>git branch</code> 或 <code>git checkout</code>）。</p><p>如果你很兴奋，这是完全可以理解的。我也很兴奋 🙂</p><p><strong>咱们开始吧：</strong></p><p>目前我们只有一个名为 <code>master</code> 的分支。要创建另一个名为 <code>test</code> 的分支（等价于执行 <code>git branch test</code>），我需要在 <code>.git\refs\heads</code> 下创建一个名为 <code>test</code> 的文件，文件的内容应该和 <code>master</code> 分支指向的那个 <strong>提交</strong> 的哈希值一致。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/edd8b9f340f94853bd71237a36c01d9a6beea22f888c2ffbffc678f42561556c/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133302e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133302e706e67" width="600" height="400" loading="lazy"></figure><p>如果我们使用 <code>git log</code>，就可以看到 <code>master</code> 和 <code>test</code> 确实是指向同一个 <strong>提交</strong>：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/aeb82c32f6b8aec6b72e97cf556d813794fcc4d14c7ae7de7119443ab45d2b19/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133312e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133312e706e67" width="600" height="400" loading="lazy"></figure><p>我们也切换到新创建的分支吧（等价于执行 <code>git branch test</code>）。为此，我们需要改变 <code>HEAD</code> 的指向，让它指向我们的新分支：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/c6368fbe7ea63d829e0458bd7dc8965b9db3b5165dca60840929cb92f42da871/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133322e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133322e706e67" width="600" height="400" loading="lazy"><figcaption>通过修改 <code>HEAD</code> 切换到 <code>test</code> 分支</figcaption></figure><p>我们可以看到：<code>git status</code> 和 <code>git log</code> 都确认 <code>HEAD</code> 现在指向的是 <code>test</code> 分支（活动分支）。</p><p>我们现在可以使用之前的命令去创建另一个文件，然后将它添加到索引：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/8f4b2fcc1a016c4107896a8420c937afddd1b6f9039f6bed708b17393178b208/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133332e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133332e706e67" width="600" height="400" loading="lazy"></figure><p>我们用上面的命令创建了一个名为 <code>test.txt</code> 的文件，文件内容为 <code>Testing</code>。我们还创建了相应的 <strong>blob</strong>，将它添加了到 <strong>索引</strong>。我们还创建了代表这个 <strong>索引</strong> 的 <strong>树对象</strong>。</p><p>现在是时候创建引用这个 <strong>树对象</strong> 的 <strong>提交</strong> 了。这一次，我们还应该声明这个提交的 <em>父提交</em>，也就是之前的那次 <strong>提交</strong>。我们用 <code>git commit-tree</code> 命令的 <code>-p</code> 开关声明父节点：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/9e8d5dcc4856cb1bac47534d84da9f726bf8c60bfa9d7a0ab7fd1fc9efae9d5f/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133362e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133362e706e67" width="600" height="400" loading="lazy"></figure><p>可以看到，我们刚刚创建了一个 <strong>提交</strong>，还有它的 <strong>树对象</strong> 和父节点：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/caf89c99c356a06a40bf4fde9dd448b67634a4ca53cd9a8fa92a6206099be0b6/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133392e706e67" width="600" height="400" loading="lazy"></figure><p><code>git log</code> 会展示我们的新 <strong>提交</strong> 吗？</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/bce85522f160fe2c207e98a856f482044854ac57587dd5175222509ae3bc691b/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133382e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3133382e706e67" width="600" height="400" loading="lazy"></figure><p>可以看到：<code>git log</code> 并没有展示任何新的东西。为什么呢？🤔 还记得 <code>git log</code> 会跟踪 <strong>分支</strong> ，查找要展示的相关提交吗？它现在给我们展示了 <code>test</code> 和它指向的那个 <strong>提交</strong>，还展示了指向同一个提交的 <code>master</code>。</p><p>没错，我们需要让 <code>test</code> 指向我们的新 <strong>提交</strong>。我们只需要稍微改变一下 <code>.git\refs\heads\test</code> 的内容：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/a8372004f7aab2ac5a0511ccddbcef7f2c9d37d08c79d3813760dd16946510f0/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3134302e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f31322f696d6167652d3134302e706e67" width="600" height="400" loading="lazy"></figure><p>成功了! 🎉🥂</p><p><code>git log</code> 找到 <code>HEAD</code>，<code>HEAD</code> 告诉它去 <code>test</code> 分支，<code>test</code> 分支指向着 <strong>提交</strong> <code>465...5e</code>，这个提交又链接到它的父 <strong>提交</strong> <code>80e...8f</code>。</p><p>尽情欣赏美吧，we <em>git</em> you。 😊</p><h1 id="--4">总结</h1><p>本文向你介绍了 <code>git</code> 的内部原理，我们一开始讲了基本对象——<strong>blob</strong>、<strong>树对象</strong> 和 <strong>提交对象</strong> 。</p><p>我们了解到 <strong>blob</strong> 持有文件的内容，<strong>树对象</strong> 是一个包含 <strong>blob 对象</strong> 和 <strong>子树对象</strong> 的目录列表，<strong>提交对象</strong> 是工作目录的一个快照，包含了一些像时间或提交信息这样的元数据。</p><p>我们接着讨论了 <strong>分支</strong>，它们不过是 <strong>提交对象</strong> 的命名引用。</p><p>我们继续描述了 <strong>工作目录</strong>，它是一个目录，有着相应的仓库。<strong>暂存区（索引）</strong> 为下一个 <strong>提交对象</strong> 持有对应的 <strong>树对象</strong>，而仓库就是一个 <strong>提交对象</strong> 的集合。</p><p>我们阐明了这些术语与 <code>git init</code>、<code>git add</code> 和 <code>git commit</code> 之间的关系，我们用这几条著名的命令创建新仓库、提交文件。</p><p>然后，我们大胆地深入 <code>git</code> 内部，停止使用上层命令，转而使用底层命令。</p><p>借助 <code>echo</code> 和 <code>git bash-object</code> 这类的底层命令，我们创建了 <strong>blob</strong>，把它添加到 <strong>索引</strong>，创建了 <strong>索引</strong> 的 <strong>树对象</strong>，以及指向这个 <strong>树对象</strong> 的 <strong>提交对象</strong>。</p><p>我们还创建了 <strong>分支</strong>，在 <strong>分支</strong> 间来回切换。为你们中那些亲身尝试这个过程的人鼓个掌！👏</p><p>希望你在跟着本文操作一遍之后，对使用 <code>git</code> 过程中背后发生的事情有了更深入的理解。</p><p><strong>感谢阅读本文！</strong> 如果你喜欢这篇文章，你可以在 <a href="http://swimm.io/" rel="nofollow">swimm.io blog</a> 阅读更多这个主题的内容。</p><p><em><a href="https://www.linkedin.com/in/omer-rosenbaum-034a08b9/" rel="nofollow">Omer Rosenbaum</a> 是 <a href="https://swimm.io/" rel="nofollow">Swimm</a> 的首席技术官、网络培训专家、Checkpoint 安全学院的创始人和<a href="https://data.cyber.org.il/networks/networks.pdf" rel="nofollow">计算机网络（希伯来语）</a>的作者</em>。</p><p>访问我的 <em><a href="https://www.youtube.com/watch?v=79jlgESHzKQ&amp;list=PL9lx0DXCC4BMS7dB7vsrKI5wzFyVIk2Kg" rel="nofollow">YouTube 频道</a></em>。</p><hr><h1 id="--5">附加资源</h1><p><code>git</code> 相关的资源已经有的很多了，我发现下面这些参考特别有用：</p><ul><li><a href="https://www.youtube.com/playlist?list=PL9lx0DXCC4BNUby5H58y6s2TQVLadV8v7" rel="nofollow">Git Internals YouTube playlist — by Brief</a></li><li><a href="https://www.youtube.com/watch?v=MYP56QJpDr4" rel="nofollow">Tim Berglund’s lecture — “Git From the Bits Up”</a></li><li><a href="https://jwiegley.github.io/git-from-the-bottom-up/" rel="nofollow">Git from the Bottom Up — by John Wiegley</a></li><li><a href="http://www.gelato.unsw.edu.au/archives/git/0512/13748.html" rel="nofollow">as promised, docs: git for the confused</a></li><li><a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects" rel="nofollow">Git Internals — Git Objects — from Pro Git book, by Scott Chacon and Ben Straub</a></li></ul><p>原文：<a href="https://www.freecodecamp.org/news/git-internals-objects-branches-create-repo/">A Visual Guide to Git Internals — Objects, Branches, and How to Create a Repo From Scratch</a>，作者：<a href="https://www.freecodecamp.org/news/author/omer/">Omer Rosenbaum</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 求职面试中的高频 Java 问题 ]]>
                </title>
                <description>
                    <![CDATA[ 大家好！在过去的几年里，我分享了很多 Java 面试题 [http://javarevisited.blogspot.sg/2015/10/133-java-interview-questions-answers-from-last-5-years.html]  和相关讨论。我的很多读者都要求我把它们整理在一起，以便他们能够在一个地方查看，因此这篇文章应运而生。 这篇文章包含了超过 50 个 Java 面试问题，涵盖了所有重要主题，例如 Java 基础知识、Java 集合框架 [https://javarevisited.blogspot.com/2011/11/collection-interview-questions-answers.html] 、Java 多线程与并发 [https://javarevisited.blogspot.com/2014/07/top-50-java-multithreading-interview-questions-answers.html#axzz5ghebTpxm] 、Java IO [https://javarevisited.blogs ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/review-these-50-questions-to-crack-your-java-programming-interview/</link>
                <guid isPermaLink="false">5fe4726339641a0517d52611</guid>
                
                    <category>
                        <![CDATA[ Java ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 面试 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 后端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Thu, 24 Dec 2020 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/1_s73cLB7vYz05f-aw_QAgFw.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>大家好！在过去的几年里，我分享了很多 <a href="http://javarevisited.blogspot.sg/2015/10/133-java-interview-questions-answers-from-last-5-years.html" rel="nofollow">Java 面试题</a> 和相关讨论。我的很多读者都要求我把它们整理在一起，以便他们能够在一个地方查看，因此这篇文章应运而生。</p><p>这篇文章包含了超过 <strong>50 个 Java 面试问题</strong>，涵盖了所有重要主题，例如 Java 基础知识、<a href="https://javarevisited.blogspot.com/2011/11/collection-interview-questions-answers.html" rel="nofollow">Java 集合框架</a>、<a href="https://javarevisited.blogspot.com/2014/07/top-50-java-multithreading-interview-questions-answers.html#axzz5ghebTpxm" rel="nofollow">Java 多线程与并发</a>、<a href="https://javarevisited.blogspot.com/2014/08/socket-programming-networking-interview-questions-answers-Java.html" rel="nofollow">Java IO</a>、<a href="https://javarevisited.blogspot.com/2012/12/top-10-jdbc-interview-questions-answers.html" rel="nofollow">JDBC</a>、<a href="http://www.java67.com/2016/08/10-jvm-options-for-java-production-application.html" rel="nofollow">JVM 内部原理</a>、<a href="http://www.java67.com/2018/06/data-structure-and-algorithm-interview-questions-programmers.html" rel="nofollow">编码问题</a>和<a href="http://www.java67.com/2015/12/top-30-oops-concept-interview-questions-answers-java.html" rel="nofollow">面向对象编程</a>等等。</p><p>这些问题也在面试中经常出现，但它们并不是很难。你可能已经在电话或现场面试中碰到过它们了。</p><p>这些问题也非常适用于回顾像多线程和集合这样的重要主题。此外，我还分享了一些很有用的进阶资源，例如 <a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fjava-the-complete-java-developer-course%2F" rel="nofollow"><strong>The Complete Java MasterClass</strong></a>，它们不仅能帮你提高 Java 技能，还能填补技能空白。</p><p>那我们还等什么呢？这里列出了一些面试中最常问的 Java 问题，这些问题对初学者和有经验的 Java 开发者都适用。</p><h3 id="-2-3-50-java-">为 2-3 年经验程序员准备的 50+ Java 面试题</h3><p>我就不浪费你的时间了，这里就是我为初级程序员（初学者和 2-3 年经验的开发者）整理的常见 <a href="http://www.java67.com/2018/03/top-50-core-java-interview-questions.html" rel="nofollow">Java 面试题</a>。</p><p><strong>1) Java 是如何实现跨平台的？（<a href="http://www.java67.com/2012/08/how-java-achieves-platform-independence.html" rel="nofollow">答案</a>）</strong><br>提示：字节码和 Java 虚拟机。</p><p><strong>2) Java 中的 <code>ClassLoader</code> 是什么？（<a href="http://javarevisited.blogspot.sg/2012/12/how-classloader-works-in-java.html#axzz59AWpr6cb" rel="nofollow">答案</a>）</strong><br>提示：它是 JVM 的一部分，用来帮类加载字节码。你可以写自己的 <code>ClassLoader</code>。</p><p><strong>3) 写一个 Java 程序判断一个数字是偶数还是奇数？（<a href="http://javarevisited.blogspot.sg/2013/04/how-to-check-if-number-is-even-or-odd.html#axzz59AWpr6cb" rel="nofollow">答案</a>）</strong><br>提示：你可以使用位运算符，例如按位与（AND）。记住，偶数的二进制格式的末尾为 0，而奇数的二进制格式末尾为 1。</p><p><strong>4) <code>ArrayList</code> 与 <code>HashSet</code> 有什么不同？（<a href="http://www.java67.com/2012/07/difference-between-arraylist-hashset-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：<code>List</code> 和 <code>Set</code> 之间的所有差异在这里都适用，例如有序性、元素的重复性、随机搜索等等。若想学习更多有关 ArrayList、HashSet 和 Java 中其它重要集合的知识，你可以查看 Richard Warburton 的<a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fjava-fundamentals-collections" rel="nofollow"><strong>Java Fundamentals: Collections</strong></a>。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/7a50edce96d734cb67d4678f02ddc9c9a682ae3c2e915974a3d1b37b65ed7f01/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f75654f774d4164354742647734626c434f704542704f644d4f7463732d6574366e505941" class="kg-image" alt="68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f75654f774d4164354742647734626c434f704542704f644d4f7463732d6574366e505941" width="600" height="400" loading="lazy"></figure><p><strong>5) 什么是单例模式中的双重检查锁（double checking locking）？（<a href="http://www.java67.com/2016/04/why-double-checked-locking-was-broken-before-java5.html" rel="nofollow">答案</a>）</strong><br>提示：两次检查一个实例是否初始化，第一次无锁，而第二次有锁。</p><p><strong>6) 你如何在 Java 中创建一个线程安全的单例？（<a href="http://javarevisited.blogspot.sg/2012/12/how-to-create-thread-safe-singleton-in-java-example.html" rel="nofollow">答案</a>）</strong><br>提示：方法有很多，例如使用枚举类、双重检查锁或嵌套静态类。</p><p><strong>7) 何时在 Java 中使用 volatile 变量？（<a href="http://www.java67.com/2012/08/what-is-volatile-variable-in-java-when.html" rel="nofollow">答案</a>）</strong><br>提示：当你需要告知 JVM 某个变量可能被多个线程修改，并提示 JVM 不要缓存该变量的值的时候。</p><p><strong>8) 何时使用 transient 变量？（<a href="http://www.java67.com/2012/08/what-is-transient-variable-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：当你想让一个实现了 <code>Serializable</code> 接口的类中的某个变量不会被序列化的时候。换句话说，当你不想在序列化对象时保存某个变量的值，你就可以用 <code>transient</code> 关键字修饰这个变量。若想了解更多有关 Java 中 transient 变量的信息，你可以查看 <a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fjava-the-complete-java-developer-course%2F" rel="nofollow"><strong>The Complete Java MasterClass</strong></a>。</p><p><strong>9) Java 中的 transient 变量和 volatile 变量有什么不同？（<a href="http://www.java67.com/2012/11/difference-between-transient-vs-volatile-modifier-variable-java.html" rel="nofollow">答案</a>）</strong><br>提示：完全不同，一个用于序列化，而另一个用于并发。</p><p><strong>10) Java 中的 Serializable 和 Externalizable 有什么区别？（<a href="http://www.java67.com/2012/10/difference-between-serializable-vs-externalizable-interface.html" rel="nofollow">答案</a>）</strong><br>提示：Externalizable 可以让你更好地控制序列化过程。</p><p><strong>11) 我们能重写 Java 中的私有方法吗？（<a href="http://www.java67.com/2013/08/can-we-override-private-method-in-java-inner-class.html" rel="nofollow">答案</a>）</strong><br>提示：不能。因为 Java 中要求被重写的父类方法在子类中是可见的，而父类的私有方法对子类不可见。</p><p><strong>12) Java 中的 <code>Hashtable</code> 与 <code>HashMap</code> 有什么不同？（<a href="http://javarevisited.blogspot.sg/2010/10/difference-between-hashmap-and.html#axzz53B6SD769" rel="nofollow">答案</a>）</strong><br>提示：不同点有几个，但最重要的是：<code>Hashtable</code> 是同步的，而 <code>HashMap</code> 不是。和 <code>HashMap</code> 相比，<code>Hashtable</code> 是历史遗留类并且很慢。</p><p><strong>13) Java 中的 <code>List</code> 与 <code>Set</code> 有什么不同？（<a href="http://javarevisited.blogspot.sg/2012/04/difference-between-list-and-set-in-java.html#axzz53n9YK0Mb" rel="nofollow">答案</a>）</strong><br>提示：<code>List</code> 有序并允许重复元素，<code>Set</code>无序并不允许重复元素。</p><p><strong>14) Java 中的 <code>ArrayList</code> 与 <code>Vector</code> 有什么不同（<a href="http://www.java67.com/2012/09/arraylist-vs-vector-in-java-interview.html" rel="nofollow">答案</a>）</strong><br>提示：不同点有很多，但最重要的是：<code>ArrayList</code> 是非同步的并且很快，而 <code>Vector</code> 是同步的并且很慢。<code>Vector</code> 也和 <code>Hashtable</code> 一样是历史遗留类。</p><p><strong>15) Java 中的 <code>Hashtable</code> 与 <code>ConcurrentHashMap</code> 有什么不同？（<a href="http://javarevisited.blogspot.sg/2011/04/difference-between-concurrenthashmap.html#axzz4qw7RoNvw" rel="nofollow">答案</a>）</strong><br>提示：后者的可扩展性更强。查看 Richard Warburton 的 <a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fjava-fundamentals-collections" rel="nofollow"><strong>Java Fundamentals: Collections</strong></a> 以了解更多相关信息。</p><p><strong>16) <code>ConcurrentHashMap</code> 是如何实现可扩展性的？（<a href="http://javarevisited.blogspot.sg/2017/08/top-10-java-concurrenthashmap-interview.html#axzz50U9xyqbo" rel="nofollow">答案</a>）</strong><br>提示：通过将 map 分割为段（segment）并且只在写操作时加锁。</p><p><strong>17) 要将某个 <code>Object</code> 用作 <code>HashMap</code> 的 <code>Key</code>，你需要重写哪两个方法？（<a href="http://www.java67.com/2013/06/how-get-method-of-hashmap-or-hashtable-works-internally.html" rel="nofollow">答案</a>）</strong><br>提示：equals 和 hashcode。</p><p><strong>18) Java 中的 wait 和 sleep 有什么不同？（<a href="http://www.java67.com/2012/08/what-are-difference-between-wait-and.html" rel="nofollow">答案</a>）</strong><br>提示：<code>wait()</code> 方法会释放锁或 monitor，而 sleep 不会。</p><p><strong>19) Java 中的 <code>notify</code> 和 <code>notifyAll</code> 有什么不同？（<a href="http://www.java67.com/2013/03/difference-between-wait-vs-notify-vs-notifyAll-java-thread.html" rel="nofollow">答案</a>）</strong><br>提示：<code>notify</code> 随机通知一个等待该锁的线程，而 <code>notifyAll</code> 通知所有等待该锁的线程。如果你非常确定只有一个线程在等待锁，就使用 <code>notify</code>，否则使用 <code>notifyAll</code> 会更好。若想了解更多线程相关的基础知识，你可以参阅 Java 大师 Heinz Kabutz 的 <a href="https://javaspecialists.teachable.com/p/threading-essentials/?product_id=539197&amp;coupon_code=SLACK100?affcode=92815_johrd7r8" rel="nofollow"><strong>Threading Essentials Mini-Course</strong></a>。</p><p><strong>20) 在 Java 中，为什么你要一起重写 hashcode 和 <code>equals()</code> ？（<a href="http://javarevisited.blogspot.sg/2015/01/why-override-equals-hashcode-or-tostring-java.html#axzz55oDxm8vv" rel="nofollow">答案</a>）</strong><br>提示：为了符合 equals 和 hashcode 的约定，当你将对象存储到集合类（例如 <code>HashMap</code> 或 <code>ArrayList</code>）中时，你就需要这么做。</p><p><strong>21) <code>HashMap</code> 的负载因子（load factor）的有什么含义？（<a href="http://www.java67.com/2017/08/top-10-java-hashmap-interview-questions.html" rel="nofollow">答案</a>）</strong><br>提示：负载因子就是触发 <code>HashMap</code> 重新调整容量的阈值，通常为 0.75，表示 <code>HashMap</code> 在容量达到了 75% 后就会调整自身大小。</p><p><strong>22) Java 中的 <code>ArrayList</code> 与 <code>LinkedList</code> 有什么区别？（<a href="http://www.java67.com/2012/12/difference-between-arraylist-vs-LinkedList-java.html" rel="nofollow">答案</a>）</strong><br>提示：与数组和链表一样，一个允许随机搜索而另一个不允许。链表上的插入和删除很容易，而数组上的查询很容易。若想了解更多有关 Java 数据结构的信息，你可以查看 Richard Warburton 在 Pluralsight 上的课程 <a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fjava-fundamentals-collections" rel="nofollow"><strong>Java Fundamentals: Collections</strong></a>。</p><p><strong>23) Java 中的 <code>CountDownLatch</code> 与 <code>CyclicBarrier</code> 有什么不同？（<a href="http://www.java67.com/2012/08/difference-between-countdownlatch-and-cyclicbarrier-java.html" rel="nofollow">答案</a>）</strong><br>提示：你可以在屏障（barrier）被打破之后重用 <code>CyclicBarrier</code>，但你不能在计数达到零之后重用 <code>CountDownLatch</code>。</p><p><strong>24) 在 Java 中，何时使用 <code>Runnable</code> 而不是 <code>Thread</code> ?（<a href="http://www.java67.com/2016/01/7-differences-between-extends-thread-vs-implements-Runnable-java.html" rel="nofollow">答案</a>）</strong><br>提示：永远都使用 <code>Runnable</code>。</p><p><strong>25) 为什么说 Java 中的枚举类（Enum）是类型安全的？（<a href="http://www.java67.com/2014/04/what-java-developer-should-know-about-Enumeration-type-in-Java.html" rel="nofollow">答案</a>）</strong><br>提示：这意味着你不能将其它枚举类型的实例赋值给一个枚举变量。例如，如果你已经有了一个叫做 <code>DayOfWeek</code> 的枚举变量，你就不能再将 <code>DayOfMonth</code> 的值赋给它了。</p><p><strong>26) Java 中 Integer 的自动装箱是如何实现的？（<a href="http://www.java67.com/2014/04/what-java-developer-should-know-about-Enumeration-type-in-Java.html" rel="nofollow">答案</a>）</strong><br>提示：通过使用 <code>valueOf()</code> 方法。</p><p><strong>27) <code>PATH</code> 与 <code>ClassPath</code> 有什么区别？（<a href="http://www.java67.com/2012/08/what-is-path-and-classpath-in-java-difference.html" rel="nofollow">答案</a>）</strong><br>提示：<code>PATH</code> 由操作系统使用，而 <code>Classpath</code> 是 JVM 用来定位 Java 二进制文件（例如 JAR 文件或 Class 文件）的。查看 <strong><a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fjava-fundamentals-core-platform" rel="nofollow">Java Fundamentals: The Core Platform</a></strong> 以了解更多有关 <code>PATH</code>、<code>Classpath</code> 和其它 Java 环境变量的信息。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/788714bddf839642e2aaac0d674a4cf471503b6ac7397bdc456e1913df6e5041/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f696f2d6c504536376f4d47316f42683230344c76506d363174376b41634c4676702d4236" class="kg-image" alt="68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f696f2d6c504536376f4d47316f42683230344c76506d363174376b41634c4676702d4236" width="600" height="400" loading="lazy"></figure><p><strong>28) Java 中方法的重载与重写有什么区别？（<a href="http://www.java67.com/2015/08/top-10-method-overloading-overriding-interview-questions-answers-java.html" rel="nofollow">答案</a>）</strong><br>提示：重写发生在子类中，而重载发生在同一个类中。此外，重写发生在运行时，而重载在编译时就解析了。</p><p><strong>29) 在 Java 中，如何防止一个类被继承？（<a href="http://www.java67.com/2017/06/10-points-about-final-modifier-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：直接把该类的构造函数设置为私有的。</p><p><strong>30) 如何防止你的类被客户端使用？（<a href="http://javarevisited.blogspot.sg/2016/01/why-jpa-entity-or-hibernate-persistence-should-not-be-final-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：将构造函数设置为私有，或直接在构造函数中抛出一个异常。</p><p><strong>31) Java 中的 <code>StringBuilder</code> 和 <code>StringBuffer</code> 有什么不同？（<a href="http://www.java67.com/2016/10/5-difference-between-stringbuffer.html" rel="nofollow">答案</a>）</strong><br>提示：<code>StringBuilder</code> 不是同步的，而 <code>StringBuffer</code> 是同步的。</p><p><strong>32) Java 中的多态与继承有什么区别？（<a href="http://www.java67.com/2014/04/difference-between-polymorphism-and-Inheritance-java-oops.html" rel="nofollow">答案</a>）</strong><br>提示：继承允许代码重用并能建立类之间的关系，这正是多态所需要的。多态提供动态行为。查看 <a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fjava-fundamentals-object-oriented-design" rel="nofollow"><strong>Java Fundamentals: Object-Oriented Design</strong></a> 以了解更多 OOP 的特性。</p><p><strong>33) 在 Java 中，我们能重写静态方法吗？（<a href="http://www.java67.com/2012/08/can-we-override-static-method-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：不能，因为重写在运行时才解析，而静态方法调用在编译期间就解析了。</p><p><strong>34) 在 Java 中，我们能访问私有方法吗？（<a href="http://www.java67.com/2012/08/can-we-override-private-method-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：能，在同一个类中就可以，在类之外则不行。</p><p><strong>35) Java 中的接口与抽象类有什么不同？（<a href="http://www.java67.com/2017/08/difference-between-abstract-class-and-interface-in-java8.html" rel="nofollow">答案</a>）</strong><br>提示：从 <a href="https://dzone.com/articles/5-courses-to-crack-java-certification-ocajp-1z0-80" rel="nofollow">Java 8</a> 开始，二者的差异不再那么明显了。然而，一个 Java 类还是可以实现多个接口，却只能继承一个类。</p><p><strong>36) Java 中的 DOM 和 SAX 解析器有什么区别？（<a href="http://www.java67.com/2012/09/dom-vs-sax-parser-in-java-xml-parsing.html" rel="nofollow">答案</a>）</strong><br>提示：DOM 将整个 XML 文件都载入内存，而 SAX 没有这么做。SAX 是一个基于事件的解析器，可以解析大文件，但 DOM 更快，是小文件的首选。</p><p><strong>37) Java 中的 throw 和 throws 关键字有什么区别？（<a href="http://www.java67.com/2012/10/difference-between-throw-vs-throws-in.html" rel="nofollow">答案</a>）</strong><br>提示：throws 声明一个方法在出错时可能抛出的异常，而 throw 关键字实际上会抛出一个异常。查看 <a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fjava-fundamentals-exception-handling" rel="nofollow"><strong>Java Fundamentals: Exception Handling</strong></a> 以了解更多有关 Java 异常处理的信息。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/47510df43dbafa457c335fa2e44f1f17c0ccbfa2c6034b0d07a77655381891f5/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f5153714b442d623937447233366b5669563165546476714e564e67645a5270353244376e" class="kg-image" alt="68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f5153714b442d623937447233366b5669563165546476714e564e67645a5270353244376e" width="600" height="400" loading="lazy"></figure><p><strong>38) Java 中的 fail-safe 和 fail-fast 迭代器有什么区别？（<a href="http://www.java67.com/2015/06/what-is-fail-safe-and-fail-fast-iterator-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：fail-safe 不会抛出 <code>ConcurrentModificationException</code>，而 <code>fail-fast</code> 在迭代集合的过程中检测到集合被修改时会抛出这个异常。</p><p><strong>39) Java 中的 Iterator 与 Enumeration 有什么区别？（<a href="http://javarevisited.blogspot.sg/2010/10/what-is-difference-between-enumeration.html#axzz59AWpr6cb" rel="nofollow">答案</a>）</strong><br>提示：Iterator 还允许你在迭代时删除元素，而 Enumeration 不允许你这么做。</p><p><strong>40) Java 中的 <code>IdentityHashMap</code> 是什么？（<a href="http://www.java67.com/2016/09/difference-between-identityhashmap-weakhashmap-enummap-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：它是一个用 <code>==</code> 而不是 <code>equals()</code> 方法判断相等性的 <code>Map</code>。</p><p><strong>41) Java 中的 <code>String</code> 池是什么？（<a href="http://javarevisited.blogspot.sg/2016/07/difference-in-string-pool-between-java6-java7.html#axzz4pGGwsyna" rel="nofollow">答案</a>）</strong><br>提示：一个 <code>String</code> 字面量池。记住， JDK 7 已经将它从永久代移到堆中了。</p><p><strong>42) 在 Java 中，一个实现了 <code>Serializable</code> 接口的类可以包含不可序列化的字段吗？（<a href="http://javarevisited.blogspot.sg/2016/09/how-to-serialize-object-in-java-serialization-example.html" rel="nofollow">答案</a>）</strong><br>提示：可以，但是你需要将它设置为 static 的或者 transient 的。</p><p><strong>43) Java 中的 this 和 super 有什么区别？（<a href="http://www.java67.com/2013/06/difference-between-this-and-super-keyword-java.html" rel="nofollow">答案</a>）</strong><br>提示：this 指向的是当前实例，而 super 指向的是父类的一个实例。</p><p><strong>44) Java 中的 <code>Comparator</code> 与 <code>Comparable</code> 有什么区别？（<a href="http://www.java67.com/2013/08/difference-between-comparator-and-comparable-in-java-interface-sorting.html" rel="nofollow">答案</a>）</strong><br>提示：<code>Comparator</code> 定义的是自定义顺序，而 <code>Comparable</code> 定义的是对象的自然顺序，例如 <code>String</code> 的字母顺序。若想了解更多有关 Java 中排序的信息，你可以查看 <strong><a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fjava-the-complete-java-developer-course%2F" rel="nofollow">The Complete Java MasterClass</a></strong>。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/5b18ba36d2efb387a444d8d3f2b736ba6d13a63efd23eb972e4a476bd83f05cb/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f444f4347467464544d686a6a3366615241695136395a535478663270666679726f466676" class="kg-image" alt="68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f444f4347467464544d686a6a3366615241695136395a535478663270666679726f466676" width="600" height="400" loading="lazy"></figure><p><strong>45) Java 中的 <code>java.util.Date</code> 和 <code>java.sql.Date</code> 有什么不同？（<a href="http://javarevisited.blogspot.sg/2012/04/difference-between-javautildate-and.html" rel="nofollow">答案</a>）</strong><br>提示：前者既包含日期又包含时间，而后者只包含日期部分。</p><p><strong>46) 为什么 Java 中的 wait 和 notify 方法被声明在 <code>Object</code> 类中？（<a href="http://javarevisited.blogspot.sg/2012/02/why-wait-notify-and-notifyall-is.html" rel="nofollow">答案</a>）</strong><br>提示：因为它们需要锁，而这个锁只能从对象上获取。</p><p><strong>47) 为什么 Java 不支持多继承？（<a href="http://javarevisited.blogspot.sg/2011/07/why-multiple-inheritances-are-not.html" rel="nofollow">答案</a>）</strong><br>提示：因为多继承在 C++ 中使用很糟糕。但是在 Java 8 中，多继承在某种程度上是支持的——现在的 Java 中只是不支持 <code>Type</code> 的多继承。</p><p><strong>48) Java 中的受检异常和非受检异常有什么区别？（<a href="http://javarevisited.blogspot.sg/2011/12/checked-vs-unchecked-exception-in-java.html" rel="nofollow">答案</a>）</strong><br>提示：如果是受检异常，你必须使用 catch 块进行处理。而非受检异常的没这个限制，编译过程中也不会出现任何问题。</p><p><strong>49) Java 中的错误（Error）和异常（Exception）有什么区别？（<a href="http://www.java67.com/2012/12/difference-between-error-vs-exception.html" rel="nofollow">答案</a>）</strong><br>提示：我打字打累了，直接看答案吧。</p><p><strong>50) Java 中的竞态条件（Race condition）和死锁（Deadlock）有什么区别？（<a href="http://javarevisited.blogspot.sg/2012/02/what-is-race-condition-in.html#axzz59AbkWuk9" rel="nofollow">答案</a>）</strong><br>提示：二者都是并发程序中出现的问题，前者发生在线程调度的过程中，而后者是糟糕的代码导致的。查看 <a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fmultithreading-and-parallel-computing-in-java%2F" rel="nofollow">Multithreading and Parallel Computing in Java</a> 以了解更多有关死锁、竞态条件和其它多线程问题的信息。</p><h3 id="-">结语</h3><p>感谢你阅读本文！祝你面试顺利！虽然面试很难，这份路线图的指导会让你离 <a href="https://hackernoon.com/10-free-courses-to-learn-docker-for-programmers-and-devops-engineers-7ff2781fd6e0" rel="nofollow">DevOps 工程师</a> 更进一步。</p><p>如果你喜欢这篇文章，请把它分享给你的朋友和同事，别忘了在 Twitter 上关注 <a href="https://twitter.com/javinpaul" rel="nofollow">javinpaul</a> 哦！</p><h4 id="--1">更多资源</h4><ul><li><a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fjava-interview-questions-and-answers%2F" rel="nofollow">Java 面试指南：200+ 面试问题和答案</a></li><li><a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fspring-interview-questions-and-answers%2F" rel="nofollow">Spring 框架面试指南：200+ 问题和答案</a></li><li><a href="https://pluralsight.pxf.io/c/1193463/424552/7490?u=https%3A%2F%2Fwww.pluralsight.com%2Fcourses%2Fdeveloper-job-interviews" rel="nofollow">John Sonmez：如何准备求职面试</a></li><li><a href="http://www.amazon.com/Java-Programming-Interviews-Exposed-Markham/dp/1118722868?tag=javamysqlanta-20" rel="nofollow">Markham：Java 面试揭秘</a></li><li><a href="http://www.amazon.com/Cracking-Coding-Interview-6th-Edition/dp/0984782850/?tag=javamysqlanta-20" rel="nofollow">通关编程面试 —— 189 个问题和答案</a></li><li><a href="https://click.linksynergy.com/fs-bin/click?id=JVFxdTr9V80&amp;subid=0&amp;offerid=323058.1&amp;type=10&amp;tmpid=14538&amp;RD_PARM1=https%3A%2F%2Fwww.udemy.com%2Fdata-structure-and-algorithms-analysis%2F" rel="nofollow">求职面试中的数据结构与算法分析</a></li><li><a href="http://javarevisited.blogspot.sg/2015/10/133-java-interview-questions-answers-from-last-5-years.html" rel="nofollow">过去五年中的 130+ Java 面试问题</a></li></ul><p><strong>P.S. —</strong> 如果你需要一些 <strong>免费</strong> 的 Java 学习资源，你可以从 <a href="http://www.java67.com/2018/08/top-10-free-java-courses-for-beginners-experienced-developers.html" rel="nofollow"><strong>free Java courses</strong></a> 开始。</p><p><strong>P. S. S. —</strong> 我还没有回答图片“How many String objects are created in the code?”中的那个面试问题。你能试着回答一下吗？</p><p>原文：<a href="https://www.freecodecamp.org/news/review-these-50-questions-to-crack-your-java-programming-interview-69d03d746b7f/">Review these 50 questions to crack your Java programming interview</a>，作者：javinpaul</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 拥有 20 年自由职业经验的开发者分享：自由职业者如何找到工作？ ]]>
                </title>
                <description>
                    <![CDATA[ 无论你是初来乍到的开发新人，还是有一定经验的开发者，你可能都在琢磨着做些自由职业（freelance）工作。 如果你想自己闯出一片天来，你可能会遇到两个问题。你首先可能会问的是“什么是自由职业”。这可以理解，因为对不同的人来说，这个词意味着不同的东西。 你可能想问的第二个问题是“如何获得客户”。当然，这一点很重要，因为你是一个自由职业者，找不到客户会令你囊中羞涩： 好消息是，如果你正考虑做大你自己的品牌，只要你做对了，也能令你欣喜若狂： 综上所述，咱们先来回答“什么是自由职业”这个问题。然后，咱们再讨论如何在自己的城市中获得网上的和本地的客户。 如果你像我一样偏爱书面内容，就继续读下去。如果你更喜欢视频，我也准备了一个有关这些话题的视频演讲 [https://www.youtube.com/watch?v=Z63TxAJotgQ&feature=emb_logo]。 我为 freeCodeCamp 写了《自由开发者如何赚钱 [https://www.freecodecamp.org/news/tips-for-making-money-as-a-freelance-deve ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-freelancing/</link>
                <guid isPermaLink="false">5fdf439f39641a0517d52222</guid>
                
                    <category>
                        <![CDATA[ 自由职业 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nicholas Zhan ]]>
                </dc:creator>
                <pubDate>Sun, 20 Dec 2020 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/photo-1604933762161-67313106146c-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>无论你是初来乍到的开发新人，还是有一定经验的开发者，你可能都在琢磨着做些自由职业（freelance）工作。</p><p>如果你想自己闯出一片天来，你可能会遇到两个问题。你首先可能会问的是“什么是自由职业”。这可以理解，因为对不同的人来说，这个词意味着不同的东西。</p><p>你可能想问的第二个问题是“如何获得客户”。当然，这一点很重要，因为你是一个自由职业者，找不到客户会令你囊中羞涩：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-20.png" class="kg-image" alt="image-20" width="600" height="400" loading="lazy"></figure><p>好消息是，如果你正考虑做大你自己的品牌，只要你做对了，也能令你欣喜若狂：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-21.png" class="kg-image" alt="image-21" width="600" height="400" loading="lazy"></figure><p>综上所述，咱们先来回答“什么是自由职业”这个问题。然后，咱们再讨论如何在自己的城市中获得网上的和本地的客户。</p><p>如果你像我一样偏爱书面内容，就继续读下去。如果你更喜欢视频，我也准备了一个有关这些话题的<a href="https://www.youtube.com/watch?v=Z63TxAJotgQ&amp;feature=emb_logo">视频演讲</a>。</p><p>我为 freeCodeCamp 写了《<a href="https://www.freecodecamp.org/news/tips-for-making-money-as-a-freelance-developer-39fae6b76972/" rel="nofollow">自由开发者如何赚钱</a>》，还写了《<a href="https://www.freecodecamp.org/news/freelance-web-developer-guide/" rel="nofollow">自由职业者完全指南</a>》。本文会和它们有所不同，只会关注两个话题：</p><p>第一，我将发表我对成为自由开发者的个人观点。第二，我将告诉你在开始之后如何获得客户。</p><p>我将把后者分成三部分：首先，我将讨论你在尝试获得客户之前应该完成的事情。然后，我会讲如何通过在线形象（online presence）获得客户。第三部分会覆盖获得同城客户的各种方式。</p><p>这儿是本文的快速导览，你可以跳转到任一章节：</p><ol><li>成为自由开发者意味着什么</li><li>在尝试获得新客户之前，你要做什么</li><li>如何在线获得新客户</li><li>如何获得同城或同地区的新客户</li></ol><p>嗯……咱们开始吧。</p><h2 id="-">成为自由开发者意味着什么</h2><p>在当今社会，“自由职业”一词的使用非常广泛（包括很多软件开发之外的领域），它已经成为了一个对不同人有着不同的含义的流行语。</p><p>如果你正想出来闯一闯，那么成为一个“自由职业者（freelancer）”就真的意味着两件事：</p><p>其一，你可能正想开始做自己的副业。其二，你可能正想为自己打工。我们来依次看一下。</p><h3 id="--1">一些人一边从事稳定工作，一边经营开发业务</h3><p>独自做副业是贴补当前工作的一个好方法。或许你正在完成 freeCodeCamp 的课程，希望一边在一家公司做开发工作，一边兼职做项目。</p><p>你可能也有一份舍不得放弃的与软件无关的工作，但你又想成为一名兼职开发者。</p><p>在这两种情况下，你的业务都是兼职活动。由于你已经有了一份全职工作，你一次与几个（可能甚至只有一个）客户合作是不太可能的。</p><p>当你走这条路线时，获得客户也是很重要的。因此，即使你没必要扩大业务规模，下面的几点也会适用于你。</p><p>做副业的一个缺点就是你需要在经营自己业务的同时从事一份全职工作。这么做的好处是有主业为你提供的稳定收入，而缺点是你会变得 <em>真的</em> 很忙。</p><p>这条路线往往会将周六和周日变成两个额外的工作日，也会给你带来不能立即响应客户的压力，因为你需要应付你的主要工作。这些只是这条路线的一些优点和缺点。</p><h3 id="--2">一些人可能选择专职经营开发业务</h3><p>很多人将副业变成了主业，他们要么辞去了当前所从事的软件工作，要么刚开始从事开发工作。</p><p>这样，你就能集中注意力开发自己的产品和为客户效劳。由于你没了全职工作的束缚，你的时间变得更加灵活了。</p><p>在这条路线上，一些人尽可能地扩张业务，而另一些人却只希望维持稳定的收入来源并拥有灵活的生活方式。</p><p>完全专注于自己的事情可以让你的收入大幅提高。为什么这么说呢？因为我和许多人发现为自己工作比从公司领工资要容易得多。</p><p>然而，走这条路最大的缺点就是你没有其它的收入来源，也就是说你的收入会变得不稳定。</p><p>你可能已经注意到了，上面提到的两种方式中都没有提到雇佣员工。因为一旦你有了员工，你就不再是一名“自由职业者”了——你是一位企业主。</p><p>在将来的一篇文章中（剧透一下），我会探讨如何将兼职的自由开发工作扩大成为成熟的业务。</p><p>选择哪条路线，完全由你自己决定。只要记住，根据你的自身情况、喜好以及你想要得到的一切做选择是很重要的。</p><p>现在让我们继续往下讨论。</p><h2 id="--3">在尝试获取新客户之前，你该做什么</h2><p>扩张你的业务的最好方式就是把现有客户的工作做好。不过，在你创建自己的品牌之前，不需要为此操心。</p><p>简单说一下，如果你没创建自己的品牌就出去尝试获得生意了，潜在客户可能并不会把你的话当真。<em>不要这么做。</em></p><p>因此，在你尝试获取新客户之前要完成的两个任务是：</p><ol><li>理解回头客与推荐的重要性，并且</li><li>建立自己的品牌。</li></ol><p>让我们依次来看看这几点。</p><h3 id="--4">自由开发者要想扩大业务，必须重视现有客户</h3><p>如果你向任何一位企业主（不一定是开发者）询问如何实现销售的增长，他们很有可能会立即开始谈论某种形式的营销手段。换句话说，他们的注意力全都集中在陌生人身上。</p><p>这些企业主通常将时间和其它资源花在营销上，结果就是他们没有把时间和资源用来服务现有的客户。我用一个非常专业的词来称呼这种做法：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-22.png" class="kg-image" alt="image-22" width="600" height="400" loading="lazy"></figure><p>如果你不把时间和资源专注投入到现有客户身上，他们就需要等待更长时间，才能收到自己的产品。当他们遇到问题时，等你回应的时间也会更长。因此，他们不太可能对自己收到的服务感到满意。</p><p>反过来，他们以后就不太可能再找你做事，也不太可能把你推荐给别人。</p><p>这带来的后果是灾难性的。为什么这么说呢？因为没有回头客和别人的引荐意味着你只能通过广告或者社交来获得客户。</p><p>假如你正为招揽新客户花钱或花时间（钱用来打广告，时间用来联系客户），而那耗费的金钱和时间会让你的利润变低。</p><p>首先，假设你为做一个网站收费三千美元，但为了找到客户，你将二百五十美元投入了市场营销，这意味着你的收益只有两千七百五十美元。</p><p>其次，假设你收费三千美元并能在十五个小时内做完，那就是时薪两百美元。但是如果你花了二至三个小时联系客户，你就需要考虑这两三个小时对你的时薪的影响了。</p><p>你需要努力赚钱才能承担这些财务开销和时间损失。但如果你建立了推荐基础和回头客基础，这就不是问题了。</p><p>我们先来看看当你专注于现有客户时，事情会怎么发展。没错，你为获得客户而花费了某种形式的资源，但是当你将来需要其它东西的时候，这个客户很有可能会再来找你。这意味着你在没有花费任何额外的资源的情况下就获得了额外的活儿。</p><p>其次，他们会把你推荐给潜在的新客户——意味着你不花费 <em>任何</em> 时间或资源就可以获得新的生意。这会抬高你的利润率，带来指数增长，助你财源广进：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-23.png" class="kg-image" alt="image-23" width="600" height="400" loading="lazy"></figure><p>我用一个自己的例子来解释。</p><p>我在 2013 年为一位律师做了一个网站。她对我提供的服务感到非常满意，并且在六个月之后就让我为一个她即将涉足的专业法律领域建了第二个网站。到现在为止，我已经持续为那位律师提供了好几年的维护服务了。</p><p>重要的是，这个律师给我推荐了另外两个人。他们中的第一个人除了雇我创建他们最初的产品外，还雇我进行持续支持与维护。</p><p>因此，我把时间用在出去找一个客户（那位律师）上面，然后我花在与一个人见面上的时间给我带来了三个不同的网站建设工作以及额外的维护服务。</p><p>理由很很明显，这比出去见三个不同的人并获得三份不同的工作更能获利。当你接到一笔业务（我的例子中的那位律师）并在一段时间内将其变成好几份工作，你的生意就会暴涨。</p><p>建立推荐基础意味着要先将你的精力集中在现有客户身上。做法很简单，如果你要为或者能为一位当前客户做点什么，去做就是了。如果你在周末还有剩余时间的话，你可以把这些时间用来找寻新的客户。</p><p>关于“当前客户优先”对你的业务增长的重要性，我怎么强调都不够。</p><h3 id="--5">自由开发者在尝试获取客户之前应该建立自己的品牌</h3><p>作为一名自由开发者，在尝试找新客户之前，你还应该做的一件事情就是建立自己的品牌。</p><p>好好理解一下为什么你需要把自己视为一家小企业的负责人。</p><p>假设你在当地有一家面包房，然后有人走进来说他们可以为你的面包房提供网站和应用开发服务。如果那位开发者连自己的网站都没有，没有作品集、没有在线评价、没有名片，而只有用于工作的个人邮箱，你是不会把他当真的。</p><p>相反，在尝试与客户见面之前，你最好把这些东西都准备好。</p><p>你的首要任务是为自己的业务建立一个网站，然后在上面放上你的作品集（即使你现在没有任何客户，你也是可以有作品集的）。</p><p>在整合网站内容这件事情上，你可以自己做，或者如果你想节省时间的话，也可以使用 <a href="https://html5up.net/" rel="nofollow">html5up</a> 提供的模板（如果你使用了其中的模板，确保你遵循了知识共享许可）。</p><p>对于作品集，我建议你至少放五到六个项目进去。如果你现在一个项目也没有完成过，你也可以把你制作的原型放进去。</p><p>举个例子，你可以为一个虚拟的面包房创建一个网站并将它放到你的作品集中。你只需要保证，当某人从你的作品集上点击那个网站时，他们可以清晰地看到一个示例并知道那并不是真实的业务。</p><p>当潜在客户寻找候选人时，拥有一个看起来很专业的网站和一份高质量的作品集的你会显得更加合适。</p><p>你需要马上做的第二件事情是：为你的业务设立线上评论主页。每当有客户对你感到满意时，请他们在线上给你留下好评非常重要，因为这些评价会增大你在将来被其它客户雇佣的可能性。</p><p>在我看来，展示评价的最重要的两个地方分别是 Google 和 Facebook，所以你需要为自己的新品牌注册一个 <a href="https://www.google.com/business/" rel="nofollow">Google my Business</a> 账户，或者为它创建一个 Facebook 页面。</p><p>在你做完了一个项目之后，如果客户很明显地表现出对你的服务感到满意，你就可以给他们发送这些评价页面的链接，以便他们可以给你留下好评。</p><p>在推销自己之前要做的最后一步就是设立一个品牌邮箱、订购名片并准备一个办公用的电话号码。</p><p>对于名片，我建议你走简洁路线，比如使用像 Vista Print 这样的服务。设立的邮箱最好是一目了然的。</p><p>至于电话号码，我会使用像 Google Voice 这样的免费服务，这类服务会给你一个专用号码，然后你可以通过手机接听这个号码的电话。完成了所有这些事项之后，你就做好准备了，现在开始做你的业务吧。</p><h2 id="--6">自由开发者如何在线上获得客户</h2><p>一个良好的网络形象能给你这个自由开发者带来源源不断的业务。然而，在建立你的在线形象时，以正确的方式去做是很重要的。</p><p>与在业务上投入的时间和资源不同，我强烈建议你在网络形象上投资。</p><p>因为投资这个点太关键了，它将是我在这个部分讨论的第一个点。我稍后会讨论如何针对当地市场而进行的网站优化，还会简单讲几个关于获取在线评价的点。</p><h3 id="--7">你应该在你的在线形象上投资，而不是在其上花钱</h3><p>我最感激的事情之一就是：在我开展业务的初期，我就明白了投资与支出的区别。</p><p>它们的概念很简单。当你在网络形象上投资时，你总会在一天结束时拥有点自己的东西。这些拥有物的形式可以是博客文章、Youbute 视频等等。这些资产不需要你再花任何额外的金钱或时间来维持，因为没有人能够从你手中夺走它们。</p><p>相比之下，在网络形象上花钱通常包括从第三方租广告空间（比如点击付费（pay-per-click）广告、Facebook 广告等等），</p><p>在自己的网络形象上投资可以让你的收益快速增长：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/12/image-24.png" class="kg-image" alt="image-24" width="600" height="400" loading="lazy"></figure><p>然而，简单地在它上面砸钱不仅会让你苦苦挣扎，还会轻而易举地将你的业务变得枯燥无味。</p><p>我们来看看为什么。</p><p>假设你这个月在广告上投入了一千美元，然后它给你带来了一万美元的收益。这看起来很容易，也比较容易让人兴奋。</p><p>但是这有一个问题：你用来打广告的一千美元现在已经是泼出去的水了，除了最初的一万美元，它不会再给你带来任何收益。你下个月还需要继续花钱做广告，否则你的收益就会变成零。</p><p>几乎可以肯定的是，依靠付费广告的网络形象会让你掉进不断花钱打广告的无底洞。和拥有你自己的市场资产相比，这就差远了。</p><p>为了展示拥有自己网络形象的价值，我要举一个个人例子。</p><p>我之前的品牌在 2020 年五月份被收购了。这些年来，我已经写了大概四百篇以我的潜在客户为目标的博客文章。从网站的建立到被收购的时间内，我最好的那篇博客已经收到了超过了一万次的点击量。</p><p>如果我一直使用点击计费的模式招揽客户，我的钱可能就花在了某个点击一次十美元的地方。因此 <em>一篇</em> 收获了一万次点击的博文相当于给我自己的业务做了十万美元的广告（$10 x 10,000）。</p><p>我大概花了总共五至六个小时的研究并撰写那篇文章。然而，除了最开始投入的时间，我就没有再在那篇文章上花其它的时间了——我已经拥有了它。</p><p>这与不会让你拥有任何东西的付费广告截然不同。如果你拥有自己的在线形象，你不仅能迅速扩张业务，还能避免大量反复出现的营销成本。</p><p>再次强调，你拥有的资产可以体现为多种形式。除了博文，你也可以考虑下 Youtube 视频和能用来吸引潜在客户的其它媒体。</p><p>我想强调的一点就是：你 <em>可以</em> 创建你自己将会拥有的内容。这些年来，我对很多开发者这么讲过。这些开发者不写任何博客，也不制作任何视频，原因居然是他们觉得做这些很不自在。</p><p>虽然我很理解他们，但对你来说，懂得“为自己工作意味着你要做很多令自己不爽的事情”这个道理至关重要。</p><p>如果你不愿制作自己的网络内容，还是选择依靠广告的话，你也是可以以一名自由开发者的身份赚钱的。然而，那些钱远远比不上你稍微跳出舒适区并进行常规内容创作所挣到的钱。</p><p>说完这些，咱们继续向前，看一看该如何打造你的网络形象。</p><h3 id="--8">你需要针对目标市场对网络形象进行优化</h3><p>我见过很多将所有业务都放在一个网站上的独立开发者，他们没有特定的市场偏好。这些网站通常会显得过于宽泛或模糊。</p><p>这样的网站可能只是在简单地告诉别人“我是一个写网站的开发者”，或者类似的信息。他们通常将网站会链接到一份含有各种各样项目的作品集，作品集罗列着他们熟悉的语言和框架，就是这样。</p><p>相反，最好是让你的网站展示一个你能做的市场，并在网站上做相关优化。</p><p>接下来的几个月，我会在 freeCodeCamp 写更多关于针对搜索引擎优化网站的文章（敬请期待）。现在，我建议你在建立自己的网站之前先熟悉 <a href="https://support.google.com/webmasters/answer/7451184" rel="nofollow">Google 的 SEO 入门指南</a>。然后，找一个你觉得你可以胜任的细分市场，并针对它优化你的网站。</p><p>为此，确保你的网站能清晰地说明你提供的服务和你是干什么的。</p><p>我知道这听起来可能有一点模糊。然而，网站的大部分内容应该是由你的目标工作类型和地理位置所决定的。为了讲得更清楚一点，我将用自己举例子。</p><p>我尽量把业务集中在中小型网站和应用软件的构建上（我之前已经写了选择市场定位的重要性），我的网站专注于俄亥俄州及其各个城市。</p><p>我的网络形象之所以只关注我的家乡，有两个原因：第一，如果我尝试争夺全国范围内的 Google 搜索量，竞争就会变得很激烈，而追求家乡的市场则要实用得多。</p><p>第二，虽然我能收到很多来自州外客户的电话并为全国范围内的客户做产品，但是许多客户倾向于在本地找开发者。此外，我的网站完全专注在网站和应用程序的开发上，并没有涵盖我能做的所有事情。</p><p>那么，这么做给我带来了什么呢？嗯……当我用 Google 搜索“Ohio website design”时，我发现我的网站排在第一位。这意味着即使我不付费做任何广告，也会有潜在客户给我打电话。我也没有为我的上一个品牌（2020 年初被收购）支付广告费用。</p><p>我的做法有让我的网站被所有的潜在客户访问吗？并没有。但是，我因具体工作而得到的目标客户比例却更高了。</p><p>这使得我的网站能比很多自由开发者的网站招揽到的生意更多。这就是为什么我选择了我的方式，而没有采用另外一种方式。另外这一种方式听起来就像：开发者几乎可以为任何地方的人做任何事情。</p><h3 id="--9">你要让满意的客户给你留下在线评论</h3><p>我在上面有提到过为业务设立在线评论页面的重要性。当你完成了某位客户的工作，请他们给你留下评论非常重要。</p><p>这么做的理由很简单。你的好评越多，你通过网站收到的联系就越多。虽然大量的好评并不会让更多的人访问你的网站，但它会增大网站访问者拿起电话并打给你的可能性。</p><p>咱们快速看看找客户要评论时的注意事项。</p><p>当你请客户为你做评价时，首先要记住的是：若你不能确定某位客户百分百会给你好评，就不要请他进行评价。你可能已经读了这句话，然后在想“这不是废话吗”。但是请相信我，有些人做的事情会让你大吃一惊的。</p><p>其次，直接叫客户给你留下评价还不够。如果你真想客户这么做，你就要给他们打电话并和他们谈谈给你评价这件事情。如果他们很乐意，你就可以给他们发一封链接到评价页面的邮件。</p><p>你会发现打电话和发邮件可以让你们彼此联系，最终会提升人们给你好评的比例。然而，你不断地让客户写评价，实际上很少有客户会真正去做的。</p><p>关于大量的好评对业务扩张的重要性，我已经强调得够多了。就像你拥有的网络资产（上面解释过）一样，这些好评也不会被别人夺走，你也不需要每个月为此支付费用。</p><p>现在我们来看看如何从本地市场获得工作吧，这些方法与网站的做法不同。</p><h2 id="--10">自由开发者如何获得本地客户</h2><p>正如我刚才解释的那样，一个良好的网络形象实际上并不怎么会给你带来很多本地客户。然而，为了获得本地的客户，你还有一些其它的事情可以做。</p><p>这些事情包括与更大的开发工坊（development shop）谈外包/合同机会，走出去和每个潜在客户谈一谈，以及参加网络集会。</p><p>我们迅速看一下每种方法更多细节。</p><p>当你从其他开发者手中接一些工作时，机会可能比你认为的要多得多。一些从事大型项目的开发工坊通常愿意（或需要）将项目中的一小部分外包出去。</p><p>这有几个方面的原因，一是他们可能需要处理一次性项目，为这种事情专门雇个人没什么意义，因为项目完成后就不需要雇的人了，所以外包比较合适。</p><p>二是更大的工坊可能会存在“中间地带”：他们现有的员工数量不足以应付，但又不足以让他们雇佣新人。同样，也会有人在这种情况下选择外包。自由开发者从处于这种情况下的工坊获得工作是很普遍的。</p><p>获取这种合同制工作的最佳方式就是：主动去联系当地的大型开发工坊，然后把你自己介绍给他们。同样（如上所述），在联系前，你需要准备好一个网站，一个作品集等等，否则他们不会认真对待你。</p><p>很多通过这种方式进行联系的自由开发者只是给大型开发工坊的老板发邮件，我认为这么做是错误的。相反，你应该确保你的方式更加个性化。</p><p>我建议你直接给执行领导打电话，告诉他你是谁，然后询问你是否能给他发送一封能表明你可以胜任外包工作的自荐信和简历。</p><p>重要的是，不要停滞不前。如果对方没有立即回信，我差不多会每月打一次电话去跟进。坚持用这种方式跟进，直到你烦了他们整整一年或者他们叫你走开。展示出你做事很有条理并且你很执着，然后你将通过这种方式获得工作。</p><p>另一种获得同城客户的好方法很简单，就是简单地同他们一一会面。这意味着你要拜访当地的公司，讨论网络服务，等等。</p><p>然而，很多开发者都在用错误的方式做这件事。不要只是挨家挨户地去问，而是先列一个你打算拜访的公司清单，然后仔细研究。看看他们是否有一个网站，好好想一下你如何能提升他们当前的网络形象，记得也花点时间去研究下他们的竞争对手。</p><p>当你去见某人时，等待通知的时间会很长很长。与对当地开发工坊的做法一样，在被明确拒绝之前，<em>不要</em> 害怕去进行跟进。</p><p>获取同城客户的第三个选择就是参加社交活动，这也是我之前在 freeCodeCamp 的文章中推荐过的做法。这个方法只适用于小部分自由开发者，因为很多人会对我刚描述的那些比较直接的做法感到不舒服。</p><p>正如我在讲“内容制作”时所说的那样，如果你想让你的业务更上一层楼，跳出你的舒适区很重要。虽然加入像 <a href="https://www.bni.com/" rel="nofollow">BNI</a> 这样的网络组织也会有结果，但我相信更直接的方式更利于获得客户。这真的取决于你愿意在舒适区外走多远。</p><h2 id="--11">结语</h2><p>这篇文章绝不只是一份关于“如何在线上或在社区里获得业务”的独家指南，我在上面描述的方法对我的业务颇有帮助，并且还让我之前的品牌被收购了。</p><p>我要讲的最后一点是：你的网络形象和本地影响力是你投入精力的结果。如果你愿意跳出你的舒适区并花时间实践我上面讲的方法，你就会走在竞争对手之前了。</p><h3 id="--12">关于我</h3><p>我是 <a href="https://www.modern-website.design/" rel="nofollow">Modern Website Design</a> 的共同创始人，我很喜欢阅读和撰写与经营自己的业务有关的东西。如果想知道我在干什么，可以<a href="https://twitter.com/Luke_Ciciliano" rel="nofollow">在 Twitter 上关注我</a>。</p><p>原文：<a href="https://www.freecodecamp.org/news/what-is-freelancing/">What is Freelancing? How to Find Freelance Jobs Online And Clients in Your City</a>，作者：Luke Ciciliano</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
