mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-30 07:06:42 +08:00
868 lines
50 KiB
HTML
868 lines
50 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>029 编程范式游记(2)- 泛型编程.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/000 开篇词 洞悉技术的本质,享受科技的乐趣.md.html">000 开篇词 洞悉技术的本质,享受科技的乐趣</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/001 程序员如何用技术变现(上).md.html">001 程序员如何用技术变现(上)</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/002 程序员如何用技术变现(下).md.html">002 程序员如何用技术变现(下)</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/003 Equifax信息泄露始末.md.html">003 Equifax信息泄露始末</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/004 从Equifax信息泄露看数据安全.md.html">004 从Equifax信息泄露看数据安全</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/005 何为技术领导力.md.html">005 何为技术领导力</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/006 如何拥有技术领导力.md.html">006 如何拥有技术领导力</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/007 推荐阅读:每个程序员都该知道的事.md.html">007 推荐阅读:每个程序员都该知道的事</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/008 Go语言,Docker和新技术.md.html">008 Go语言,Docker和新技术</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/009 答疑解惑:渴望、热情和选择.md.html">009 答疑解惑:渴望、热情和选择</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/010 如何成为一个大家愿意追随的Leader?.md.html">010 如何成为一个大家愿意追随的Leader?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/011 程序中的错误处理:错误返回码和异常捕捉.md.html">011 程序中的错误处理:错误返回码和异常捕捉</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/012 程序中的错误处理:异步编程和最佳实践.md.html">012 程序中的错误处理:异步编程和最佳实践</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/013 魔数 0x5f3759df.md.html">013 魔数 0x5f3759df</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/014 推荐阅读:机器学习101.md.html">014 推荐阅读:机器学习101</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/015 时间管理:同扭曲时间的事儿抗争.md.html">015 时间管理:同扭曲时间的事儿抗争</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/016 时间管理:投资赚取时间.md.html">016 时间管理:投资赚取时间</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/017 故障处理最佳实践:应对故障.md.html">017 故障处理最佳实践:应对故障</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/018 故障处理最佳实践:故障改进.md.html">018 故障处理最佳实践:故障改进</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/019 答疑解惑:我们应该能够识别的表象和本质.md.html">019 答疑解惑:我们应该能够识别的表象和本质</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/020 分布式系统架构的冰与火.md.html">020 分布式系统架构的冰与火</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/021 从亚马逊的实践,谈分布式系统的难点.md.html">021 从亚马逊的实践,谈分布式系统的难点</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/022 分布式系统的技术栈.md.html">022 分布式系统的技术栈</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/023 分布式系统关键技术:全栈监控.md.html">023 分布式系统关键技术:全栈监控</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/024 分布式系统关键技术:服务调度.md.html">024 分布式系统关键技术:服务调度</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/025 分布式系统关键技术:流量与数据调度.md.html">025 分布式系统关键技术:流量与数据调度</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/026 洞悉PaaS平台的本质.md.html">026 洞悉PaaS平台的本质</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/027 推荐阅读:分布式系统架构经典资料.md.html">027 推荐阅读:分布式系统架构经典资料</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/028 编程范式游记(1)- 起源.md.html">028 编程范式游记(1)- 起源</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/左耳听风/029 编程范式游记(2)- 泛型编程.md.html">029 编程范式游记(2)- 泛型编程</a>
|
||
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/030 编程范式游记(3) - 类型系统和泛型的本质.md.html">030 编程范式游记(3) - 类型系统和泛型的本质</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/031 Git协同工作流,你该怎样选.md.html">031 Git协同工作流,你该怎样选</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/032 推荐阅读:分布式数据调度相关论文.md.html">032 推荐阅读:分布式数据调度相关论文</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/033 编程范式游记(4)- 函数式编程.md.html">033 编程范式游记(4)- 函数式编程</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/034 编程范式游记(5)- 修饰器模式.md.html">034 编程范式游记(5)- 修饰器模式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/035 编程范式游记(6)- 面向对象编程.md.html">035 编程范式游记(6)- 面向对象编程</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/036 编程范式游记(7)- 基于原型的编程范式.md.html">036 编程范式游记(7)- 基于原型的编程范式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/037 编程范式游记(8)- Go 语言的委托模式.md.html">037 编程范式游记(8)- Go 语言的委托模式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/038 编程范式游记(9)- 编程的本质.md.html">038 编程范式游记(9)- 编程的本质</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/039 编程范式游记(10)- 逻辑编程范式.md.html">039 编程范式游记(10)- 逻辑编程范式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/040 编程范式游记(11)- 程序世界里的编程范式.md.html">040 编程范式游记(11)- 程序世界里的编程范式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/041 弹力设计篇之“认识故障和弹力设计”.md.html">041 弹力设计篇之“认识故障和弹力设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/042 弹力设计篇之“隔离设计”.md.html">042 弹力设计篇之“隔离设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/043 弹力设计篇之“异步通讯设计”.md.html">043 弹力设计篇之“异步通讯设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/044 弹力设计篇之“幂等性设计”.md.html">044 弹力设计篇之“幂等性设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/045 弹力设计篇之“服务的状态”.md.html">045 弹力设计篇之“服务的状态”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/046 弹力设计篇之“补偿事务”.md.html">046 弹力设计篇之“补偿事务”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/047 弹力设计篇之“重试设计”.md.html">047 弹力设计篇之“重试设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/048 弹力设计篇之“熔断设计”.md.html">048 弹力设计篇之“熔断设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/049 弹力设计篇之“限流设计”.md.html">049 弹力设计篇之“限流设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/050 弹力设计篇之“降级设计”.md.html">050 弹力设计篇之“降级设计”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/051 弹力设计篇之“弹力设计总结”.md.html">051 弹力设计篇之“弹力设计总结”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/052 区块链技术 - 区块链的革命性及技术概要.md.html">052 区块链技术 - 区块链的革命性及技术概要</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/053 区块链技术 - 区块链技术细节 - 哈希算法.md.html">053 区块链技术 - 区块链技术细节 - 哈希算法</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/054 区块链技术 - 区块链技术细节 - 加密和挖矿.md.html">054 区块链技术 - 区块链技术细节 - 加密和挖矿</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/055 区块链技术 - 去中心化的共识机制.md.html">055 区块链技术 - 去中心化的共识机制</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/056 区块链技术 - 智能合约.md.html">056 区块链技术 - 智能合约</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/057 区块链技术 - 传统金融和虚拟货币.md.html">057 区块链技术 - 传统金融和虚拟货币</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/058 管理设计篇之分布式锁.md.html">058 管理设计篇之分布式锁</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/059 管理设计篇之配置中心.md.html">059 管理设计篇之配置中心</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/060 管理设计篇之边车模式.md.html">060 管理设计篇之边车模式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/061 管理设计篇之服务网格.md.html">061 管理设计篇之服务网格</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/062 管理设计篇之网关模式.md.html">062 管理设计篇之网关模式</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/063 管理设计篇之部署升级策略.md.html">063 管理设计篇之部署升级策略</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/064 性能设计篇之缓存.md.html">064 性能设计篇之缓存</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/065 性能设计篇之异步处理.md.html">065 性能设计篇之异步处理</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/066 性能设计篇之数据库扩展.md.html">066 性能设计篇之数据库扩展</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/067 性能设计篇之秒杀.md.html">067 性能设计篇之秒杀</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/068 性能设计篇之边缘计算.md.html">068 性能设计篇之边缘计算</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/069 程序员练级攻略(2018):开篇词.md.html">069 程序员练级攻略(2018):开篇词</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/070 程序员练级攻略(2018):零基础启蒙.md.html">070 程序员练级攻略(2018):零基础启蒙</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/071 程序员练级攻略(2018):正式入门.md.html">071 程序员练级攻略(2018):正式入门</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/072 程序员练级攻略(2018):程序员修养.md.html">072 程序员练级攻略(2018):程序员修养</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/073 程序员练级攻略(2018):编程语言.md.html">073 程序员练级攻略(2018):编程语言</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/074 程序员练级攻略:理论学科.md.html">074 程序员练级攻略:理论学科</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/075 程序员练级攻略(2018):系统知识.md.html">075 程序员练级攻略(2018):系统知识</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/076 程序员练级攻略(2018):软件设计.md.html">076 程序员练级攻略(2018):软件设计</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/077 程序员练级攻略(2018):Linux系统、内存和网络.md.html">077 程序员练级攻略(2018):Linux系统、内存和网络</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/078 程序员练级攻略(2018):异步IO模型和Lock-Free编程.md.html">078 程序员练级攻略(2018):异步IO模型和Lock-Free编程</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/079 程序员练级攻略(2018):Java底层知识.md.html">079 程序员练级攻略(2018):Java底层知识</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/080 程序员练级攻略(2018):数据库.md.html">080 程序员练级攻略(2018):数据库</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/081 程序员练级攻略(2018):分布式架构入门.md.html">081 程序员练级攻略(2018):分布式架构入门</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/082 程序员练级攻略(2018):分布式架构经典图书和论文.md.html">082 程序员练级攻略(2018):分布式架构经典图书和论文</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/083 程序员练级攻略(2018):分布式架构工程设计.md.html">083 程序员练级攻略(2018):分布式架构工程设计</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/084 程序员练级攻略(2018):微服务.md.html">084 程序员练级攻略(2018):微服务</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/085 程序员练级攻略(2018):容器化和自动化运维.md.html">085 程序员练级攻略(2018):容器化和自动化运维</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/086 程序员练级攻略(2018):机器学习和人工智能.md.html">086 程序员练级攻略(2018):机器学习和人工智能</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/087 程序员练级攻略(2018):前端基础和底层原理.md.html">087 程序员练级攻略(2018):前端基础和底层原理</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/088 程序员练级攻略(2018):前端性能优化和框架.md.html">088 程序员练级攻略(2018):前端性能优化和框架</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/089 程序员练级攻略(2018):UIUX设计.md.html">089 程序员练级攻略(2018):UIUX设计</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/090 程序员练级攻略(2018):技术资源集散地.md.html">090 程序员练级攻略(2018):技术资源集散地</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/091 程序员面试攻略:面试前的准备.md.html">091 程序员面试攻略:面试前的准备</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/092 程序员面试攻略:面试中的技巧.md.html">092 程序员面试攻略:面试中的技巧</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/093 程序员面试攻略:面试风格.md.html">093 程序员面试攻略:面试风格</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/094 程序员面试攻略:实力才是王中王.md.html">094 程序员面试攻略:实力才是王中王</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/095 高效学习:端正学习态度.md.html">095 高效学习:端正学习态度</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/096 高效学习:源头、原理和知识地图.md.html">096 高效学习:源头、原理和知识地图</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/097 高效学习:深度,归纳和坚持实践.md.html">097 高效学习:深度,归纳和坚持实践</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/098 高效学习:如何学习和阅读代码.md.html">098 高效学习:如何学习和阅读代码</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/099 高效学习:面对枯燥和量大的知识.md.html">099 高效学习:面对枯燥和量大的知识</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/100 高效沟通:Talk和Code同等重要.md.html">100 高效沟通:Talk和Code同等重要</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/101 高效沟通:沟通阻碍和应对方法.md.html">101 高效沟通:沟通阻碍和应对方法</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/102 高效沟通:沟通方式及技巧.md.html">102 高效沟通:沟通方式及技巧</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/103 高效沟通:沟通技术.md.html">103 高效沟通:沟通技术</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/104 高效沟通:好老板要善于提问.md.html">104 高效沟通:好老板要善于提问</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/105 高效沟通:好好说话的艺术.md.html">105 高效沟通:好好说话的艺术</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/106 加餐 谈谈我的“三观”.md.html">106 加餐 谈谈我的“三观”</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/左耳听风/107 结束语 业精于勤,行成于思.md.html">107 结束语 业精于勤,行成于思</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>029 编程范式游记(2)- 泛型编程</h1>
|
||
<p>上一篇文章中,我从 C 语言开始说起,聊了聊面向过程式的编程范式,相信从代码的角度你对这类型的语言已经有了一些理解。作为一门高级语言,C 语言绝对是编程语言历史发展中的一个重要里程碑,但随着认知的升级,面向过程的 C 语言已经无法满足更高层次的编程的需要。于是,C++ 出现了。</p>
|
||
<h1>C++ 语言</h1>
|
||
<p>1980 年,AT&T 贝尔实验室的<strong>Bjarne Stroustrup</strong>创建的 C++ 语言横空出世,它既可以全面兼容 C 语言,又巧妙揉和了一些面向对象的编程理念。现在来看,不得不佩服 Stroustrup 的魄力。在这里,我也向你推荐一本书,书名是《C++ 语言的设计和演化》。</p>
|
||
<p>这本书系统介绍了 C++ 诞生的背景以及初衷,书的作者就是<a href="https://book.douban.com/author/362072/">Stroustrup</a>本人,所以你可以非常详细地从语言创建者的角度了解他的设计思路和创新之旅。当然,就是在今天,C++ 这门语言也还有很多争议,这里我不细说。如果你感兴趣的话,可以看看我几年前在酷壳上发表的文章《<a href="https://coolshell.cn/articles/7992.html">C++ 的坑真的多吗?</a>》。</p>
|
||
<p>从语言角度来说,实际上早期 C++ 的许多工作是对 C 的强化和净化,并把完全兼容 C 作为强制性要求(这也是 C++ 复杂晦涩的原因,这点 Java 就干得比 C++ 彻底得多)。在 C89、C99 这两个 C 语言的标准中,有许多改进正是从 C++ 中所引进的。</p>
|
||
<p>可见,C++ 对 C 语言的贡献非常之大。是的,C++ 很大程度就是来解决 C 语言中的各种问题和各种不方便。比如:</p>
|
||
<ul>
|
||
<li>用引用来解决指针的问题。</li>
|
||
<li>用 namespace 来解决名字空间冲突的问题。</li>
|
||
<li>通过 try-catch 来解决检查返回值编程的问题。</li>
|
||
<li>用 class 来解决对象的创建、复制、销毁的问题,从而可以达到在结构体嵌套时可以深度复制的内存安全问题。</li>
|
||
<li>通过重载操作符来达到操作上的泛型。(比如,消除[上一篇文章]中提到的比较函数<code>cmpFn</code>,再比如用<code>>></code>操作符消除<code>printf()</code>的数据类型不够泛型的问题。)</li>
|
||
<li>通过模板 template 和虚函数的多态以及运行时识别来达到更高层次的泛型和多态。</li>
|
||
<li>用 RAII、智能指针的方式,解决了 C 语言中因为需要释放资源而出现的那些非常 ugly 也很容易出错的代码的问题。</li>
|
||
<li>用 STL 解决了 C 语言中算法和数据结构的 N 多种坑。</li>
|
||
</ul>
|
||
<h1>C++ 泛型编程</h1>
|
||
<p>C++ 是支持编程范式最多的一门语言,它虽然解决了很多 C 语言的问题,但我个人觉得它最大的意义是解决了 C 语言泛型编程的问题。因为,我们可以看到一些 C++ 的标准规格说明书里面,有一半以上都在说明 STL 的标准规格应该是什么样的,这说明泛型编程是 C++ 重点中的重点。</p>
|
||
<p>理想情况下,算法应是和数据结构以及类型无关的,各种特殊的数据类型理应做好自己分内的工作。算法只关心一个标准的实现。<strong>而对于泛型的抽象,我们需要回答的问题是,如果我们的数据类型符合通用算法,那么对数据类型的最小需求是什么?</strong></p>
|
||
<p>那 C++ 是如何有效解决程序泛型问题的呢?我认为有三点。</p>
|
||
<p><strong>第一,它通过类的方式来解决</strong>。</p>
|
||
<ul>
|
||
<li>类里面会有构造函数、析构函数表示这个类的分配和释放。</li>
|
||
<li>还有它的拷贝构造函数,表示了对内存的复制。</li>
|
||
<li>还有重载操作符,像我们要去比较大于、等于、不等于。</li>
|
||
</ul>
|
||
<p>这样可以让一个用户自定义的数据类型和内建的那些数据类型就很一致了。</p>
|
||
<p><strong>第二,通过模板达到类型和算法的妥协</strong>。</p>
|
||
<ul>
|
||
<li>模板有点像 DSL,模板的特化会根据使用者的类型在编译时期生成那个模板的代码。</li>
|
||
<li>模板可以通过一个虚拟类型来做类型绑定,这样不会导致类型转换时的问题。</li>
|
||
</ul>
|
||
<p>模板很好地取代了 C 时代的宏定义带来的问题。</p>
|
||
<p><strong>第三,通过虚函数和运行时类型识别</strong>。</p>
|
||
<ul>
|
||
<li>虚函数带来的多态在语义上可以让“同一类”的类型进行泛型。</li>
|
||
<li>运行时类型识别技术可以做到在泛型时对具体类型的特殊处理。</li>
|
||
</ul>
|
||
<p>这样一来,就可以写出基于抽象接口的泛型。</p>
|
||
<p>拥有了这些 C++ 引入的技术,我们就可以做到 C 语言很难做到的泛型编程了。</p>
|
||
<p>正如前面说过的,一个良好的泛型编程需要解决如下几个泛型编程的问题:</p>
|
||
<ol>
|
||
<li>算法的泛型;</li>
|
||
<li>类型的泛型;</li>
|
||
<li>数据结构(数据容器)的泛型。</li>
|
||
</ol>
|
||
<h2>C++ 泛型编程的示例 - Search 函数</h2>
|
||
<p>就像前面的<code>search()</code>函数,里面的 <code>for(int i=0; i<len; i++)</code> 这样的遍历方式,只能适用于<strong>顺序型的数据结构</strong>的方式迭代,如:array、set、queue、list 和 link 等。并不适用于<strong>非顺序型的数据结构</strong>。</p>
|
||
<p>如哈希表 hash table,二叉树 binary tree、图 graph 等这样数据不是按顺序存放的数据结构(数据容器)。所以,如果找不到一种<strong>泛型的数据结构的操作方式(如遍历、查找、增加、删除、修改……)</strong>,那么,任何的算法或是程序都不可能做到真正意义上的泛型。</p>
|
||
<p>除了<code>search()</code>函数的“遍历操作”之外,还有 search 函数的返回值,是一个整型的索引下标。这个整型的下标对于“顺序型的数据结构”是没有问题的,但是对于“非顺序的数据结构”,在语义上都存在问题。</p>
|
||
<p>比如,如果我要在一个 hash table 中查找一个 key,返回什么呢?一定不是返回“索引下标”,因为在 hash table 这样的数据结构中,数据的存放位置不是顺序的,而且还会因为容量不够的问题被重新 hash 后改变,所以返回数组下标是没有意义的。</p>
|
||
<p>对此,我们要把这个事做得泛型和通用一些。如果找到,返回找到的这个元素的一个指针(地址)会更靠谱一些。</p>
|
||
<p>所以,为了解决泛型的问题,我们需要动用以下几个 C++ 的技术。</p>
|
||
<ol>
|
||
<li>使用模板技术来抽象类型,这样可以写出类型无关的数据结构(数据容器)。</li>
|
||
<li>使用一个迭代器来遍历或是操作数据结构内的元素。</li>
|
||
</ol>
|
||
<p>我们来看一下 C++ 版的<code>search()</code>函数是什么样的。</p>
|
||
<p>先重温一下 C 语言版的代码:</p>
|
||
<pre><code class="language-c">int search(void* a, size_t size, void* target,
|
||
size_t elem_size, int(*cmpFn)(void*, void*) )
|
||
{
|
||
for(int i=0; i<size; i++) {
|
||
if ( cmpFn (a + elem_size * i, target) == 0 ) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
</code></pre>
|
||
<p>我们再来看一下 C++ 泛型版的代码:</p>
|
||
<pre><code class="language-c++">template<typename T, typename Iter>
|
||
Iter search(Iter pStart, Iter pEnd, T target)
|
||
{
|
||
for(Iter p = pStart; p != pEnd; p++) {
|
||
if ( *p == target )
|
||
return p;
|
||
}
|
||
return NULL;
|
||
}
|
||
</code></pre>
|
||
<p>在 C++ 的泛型版本中,我们可以看到:</p>
|
||
<ul>
|
||
<li>使用<code>typename T</code>抽象了数据结构中存储数据的类型。</li>
|
||
<li>使用<code>typename Iter</code>,这是不同的数据结构需要自己实现的“迭代器”,这样也就抽象掉了不同类型的数据结构。</li>
|
||
<li>然后,我们对数据容器的遍历使用了<code>Iter</code>中的<code>++</code>方法,这是数据容器需要重载的操作符,这样通过操作符重载也就泛型掉了遍历。</li>
|
||
<li>在函数的入参上使用了<code>pStart</code>和<code>pEnd</code>来表示遍历的起止。</li>
|
||
<li>使用<code>*Iter</code>来取得这个“指针”的内容。这也是通过重载 <code>*</code> 取值操作符来达到的泛型。</li>
|
||
</ul>
|
||
<p>当然,你可能会问,为什么我们不用标准接口<code>Iter.Next()</code>取代<code>++</code>, 用<code>Iter.GetValue()</code>来取代<code>*</code>,而是通过重载操作符?这样做是为了兼容原有 C 语言的编程习惯。</p>
|
||
<p>说明一下,所谓的<code>Iter</code>,在实际代码中,就是像<code>vector<int>::iterator</code>或<code>map<int, string>::iterator</code>这样的东西。这是由相应的数据容器来实现和提供的。</p>
|
||
<p>注:下面是 C++ STL 中的<code>find()</code>函数的代码。</p>
|
||
<pre><code class="language-c++">template<class InputIterator, class T>
|
||
InputIterator find (InputIterator first, InputIterator last, const T& val)
|
||
{
|
||
while (first!=last) {
|
||
if (*first==val) return first;
|
||
++first;
|
||
}
|
||
return last;
|
||
}
|
||
</code></pre>
|
||
<h2>C++ 泛型编程示例 - Sum 函数</h2>
|
||
<p>也许你觉得到这一步,我们的泛型设计就完成了。其实,还远远不够。<code>search</code>函数只是一个开始,我们还有很多别的算法会让问题变得更为复杂。</p>
|
||
<p>我们再来看一个<code>sum()</code>函数。</p>
|
||
<p>先看 C 语言版:</p>
|
||
<pre><code class="language-c">long sum(int *a, size_t size) {
|
||
long result = 0;
|
||
for(int i=0; i<size; i++) {
|
||
result += a[i];
|
||
}
|
||
return result;
|
||
}
|
||
</code></pre>
|
||
<p>再看一下 C++ 泛型的版本:</p>
|
||
<pre><code class="language-c++">template<typename T, typename Iter>
|
||
T sum(Iter pStart, Iter pEnd) {
|
||
T result = 0;
|
||
for(Iter p=pStart; p!=pEnd; p++) {
|
||
result += *p;
|
||
}
|
||
return result;
|
||
}
|
||
</code></pre>
|
||
<p>你看到了什么样的问题?这个代码中最大的问题就是 <code>T result = 0;</code> 这条语句:</p>
|
||
<ul>
|
||
<li>那个<code>0</code>假设了类型是<code>int</code>;</li>
|
||
<li>那个<code>T</code>假设了 Iter 中出来的类型是<code>T</code>。</li>
|
||
</ul>
|
||
<p>这样的假设是有问题的,如果类型不一样,就会导致转型的问题,这会带来非常 buggy 的代码。那么,我们怎么解决呢?</p>
|
||
<h2>C++ 泛型编程的重要技术 - 迭代器</h2>
|
||
<p>我们知道<code>Iter</code>在实际调用者那会是一个具体的像<code>vector<int>::iterator</code>这样的东西。在这个声明中,<code>int</code>已经被传入<code>Iter</code>中了。所以,定义<code>result</code>的<code>T</code>应该可以从<code>Iter</code>中来。这样就可以保证类型是一样的,而且不会有被转型的问题。</p>
|
||
<p>所以,我们需要精心地实现一个“迭代器”。下面是一个“精简版”的迭代器(我没有把 C++ STL 代码里的迭代器列出来,是因为代码太多太复杂,我这里只是为了说明问题)。</p>
|
||
<pre><code class="language-c++">template <class T>
|
||
class container {
|
||
public:
|
||
class iterator {
|
||
public:
|
||
typedef iterator self_type;
|
||
typedef T value_type;
|
||
typedef T* pointer;
|
||
typedef T& reference;
|
||
|
||
reference operator*();
|
||
pointer operator->();
|
||
bool operator==(const self_type& rhs);
|
||
bool operator!=(const self_type& rhs);
|
||
self_type operator++() { self_type i = *this; ptr_++; return i; }
|
||
self_type operator++(int junk) { ptr_++; return *this; }
|
||
...
|
||
...
|
||
private:
|
||
pointer _ptr;
|
||
};
|
||
|
||
iterator begin();
|
||
iterator end();
|
||
...
|
||
...
|
||
};
|
||
</code></pre>
|
||
<p>上面的代码是我写的一个迭代器(这个迭代器在语义上是没有问题的),我没有把所有的代码列出来,而把它的一些基本思路列了出来。这里我说明一下几个关键点。</p>
|
||
<ul>
|
||
<li>首先,一个迭代器需要和一个容器在一起,因为里面是对这个容器的具体的代码实现。</li>
|
||
<li>它需要重载一些操作符,比如:取值操作<code>*</code>、成员操作<code>-></code>、比较操作<code>==</code>和<code>!=</code>,还有遍历操作<code>++</code>,等等。</li>
|
||
<li>然后,还要<code>typedef</code>一些类型,比如<code>value_type</code>,告诉我们容器内的数据的实际类型是什么样子。</li>
|
||
<li>还有一些,如<code>begin()</code>和<code>end()</code>的基本操作。</li>
|
||
<li>我们还可以看到其中有一个<code>pointer _ptr</code>的内部指针来指向当前的数据(注意,<code>pointer</code>就是 <code>T*</code>)。</li>
|
||
</ul>
|
||
<p>好了,有了这个迭代器后,我们还要解决<code>T result = 0</code>后面的这个<code>0</code>的问题。这个事,算法没有办法搞定,最好由用户传入。于是出现了下面最终泛型的<code>sum()</code>版函数。</p>
|
||
<pre><code class="language-c++">template <class Iter>
|
||
typename Iter::value_type
|
||
sum(Iter start, Iter end, T init) {
|
||
typename Iter::value_type result = init;
|
||
while (start != end) {
|
||
result = result + *start;
|
||
start++;
|
||
}
|
||
return result;
|
||
}
|
||
</code></pre>
|
||
<p>我们可以看到<code>typename Iter::value_type result = init</code>这条语句是关键。我们解决了所有的问题。</p>
|
||
<p>我们如下使用:</p>
|
||
<pre><code class="language-c++">container<int> c;
|
||
container<int>::iterator it = c.begin();
|
||
sum(c.begin(), c.end(), 0);
|
||
</code></pre>
|
||
<p>这就是整个 STL 的泛型方法,其中包括:</p>
|
||
<ul>
|
||
<li>泛型的数据容器;</li>
|
||
<li>泛型数据容器的迭代器;</li>
|
||
<li>然后泛型的算法就很容易写了。</li>
|
||
</ul>
|
||
<h1>需要更多的抽象</h1>
|
||
<h2>更为复杂的需求</h2>
|
||
<p>但是,还能不能做到更为泛型呢?比如:如果我们有这样的一个数据结构 Employee,里面有 vacation 就是休假多少天,以及工资。</p>
|
||
<pre><code class="language-c++">struct Employee {
|
||
string name;
|
||
string id;
|
||
int vacation;
|
||
double salary;
|
||
};
|
||
</code></pre>
|
||
<p>现在我想计算员工的总薪水,或是总休假天数。</p>
|
||
<pre><code class="language-c++">vector<Employee> staff;
|
||
//total salary or total vacation days?
|
||
sum(staff.begin(), staff.end(), 0);
|
||
</code></pre>
|
||
<p>我们的<code>sum</code>完全不知道怎么搞了,因为要累加的是<code>Employee</code>类中的不同字段,即便我们的 Employee 中重载了<code>+</code>操作,也不知道要加哪个字段。</p>
|
||
<p>另外,我们可能还会有:求平均值 average,求最小值 min,求最大值 max,求中位数 mean 等等。你会发现,算法写出来基本上都是一样的,只是其中的“累加”操作变成了另外一个操作。就这个例子而言,就是,我想计算员工薪水里面最高的,和休假最少的,或者我想计算全部员工的总共休假多少天。那么面对这么多的需求,我们是否可以泛型一些呢?怎样解决这些问题呢?</p>
|
||
<h2>更高维度的抽象</h2>
|
||
<p>要解决这个问题,我希望我的这个算法只管遍历,具体要干什么,那是业务逻辑,由外面的调用方来定义我就好了,和我无关。这样一来,代码的重用度就更高了。</p>
|
||
<p>下面是一个抽象度更高的版本,这个版本再叫<code>sum</code>就不太合适了。这个版本应该是<code>reduce</code>——用于把一个数组 reduce 成一个值。</p>
|
||
<pre><code class="language-c++">template<class Iter, class T, class Op>
|
||
T reduce (Iter start, Iter end, T init, Op op) {
|
||
T result = init;
|
||
while ( start != end ) {
|
||
result = op( result, *start );
|
||
start++;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
</code></pre>
|
||
<p>上面的代码中,我们需要传一个函数进来。在 STL 中,它是个函数对象,我们还是这套算法,但是 result 不是像前面那样去加,是把整个迭代器值给你一个 operation,然后由它来做。我把这个方法又拿出去了,所以就会变成这个样子。</p>
|
||
<p>在 C++ STL 中,与我的这个 reduce 函数对应的函数名叫 <code>accumulate()</code>,其实际代码有两个版本。</p>
|
||
<p>第一个版本就是上面的版本,只不过是用<code>for</code>语句而不是<code>while</code>。</p>
|
||
<pre><code class="language-c++">template<class InputIt, class T>
|
||
T accumulate(InputIt first, InputIt last, T init)
|
||
{
|
||
for (; first != last; ++first) {
|
||
init = init + *first;
|
||
}
|
||
return init;
|
||
}
|
||
</code></pre>
|
||
<p>第二个版本,更为抽象,因为需要传入一个“二元操作函数”——<code>BinaryOperation op</code>来做 accumulate。accumulate 的语义比 sum 更抽象了。</p>
|
||
<pre><code class="language-c++">template<class InputIt, class T, class BinaryOperation>
|
||
T accumulate(InputIt first, InputIt last, T init,
|
||
BinaryOperation op)
|
||
{
|
||
for (; first != last; ++first) {
|
||
init = op(init, *first);
|
||
}
|
||
return init;
|
||
}
|
||
</code></pre>
|
||
<p>来看看我们在使用中是什么样子的:</p>
|
||
<pre><code class="language-c++">double sum_salaries =
|
||
reduce( staff.begin(), staff.end(), 0.0,
|
||
|
||
{return s + e.salary;} );
|
||
|
||
double max_salary =
|
||
reduce( staff.begin(), staff.end(), 0.0,
|
||
|
||
{return s > e.salary? s: e.salary; } );
|
||
</code></pre>
|
||
<p>注意:我这里用了 C++ 的 lambda 表达式。</p>
|
||
<p>你可以很清楚地看到,reduce 这个函数就更通用了,具体要干什么样的事情呢?放在匿名函数里面,它会定义我,我只做一个 reduce。更抽象地来说,我就把一个数组,一个集合,变成一个值。怎么变成一个值呢?由这个函数来决定。</p>
|
||
<h3>Reduce 函数</h3>
|
||
<p>我们来看看如何使用 reduce 和其它函数完成一个更为复杂的功能。</p>
|
||
<p>下面这个示例中,我先定义了一个函数对象<code>counter</code>。这个函数对象需要一个<code>Cond</code>的函数对象,它是个条件判断函数,如果满足条件,则加 1,否则加 0。</p>
|
||
<pre><code class="language-c++">template<class T, class Cond>
|
||
struct counter {
|
||
size_t operator()(size_t c, T t) const {
|
||
return c + (Cond(t) ? 1 : 0);
|
||
}
|
||
};
|
||
</code></pre>
|
||
<p>然后,我用上面的<code>counter</code>函数对象和<code>reduce</code>函数共同来打造一个<code>counter_if</code>算法(当条件满足的时候我就记个数,也就是统计满足某个条件的个数),我们可以看到,就是一行代码的事。</p>
|
||
<pre><code class="language-c++">template<class Iter, class Cond>
|
||
size_t count_if(Iter begin, Iter end, Cond c){
|
||
return reduce(begin, end, 0,
|
||
counter<Iter::value_type, Cond>(c));
|
||
}
|
||
</code></pre>
|
||
<p>至于是什么样的条件,这个属于业务逻辑,不是我的流程控制,所以,这应该交给使用方。</p>
|
||
<p>于是,当我需要统计薪资超过 1 万元的员工的数量时,一行代码就完成了。</p>
|
||
<pre><code class="language-c++">size_t cnt = count_if(staff.begin(), staff.end(),
|
||
{ return e.salary > 10000; });
|
||
</code></pre>
|
||
<p>Reduce 时可以只对结构体中的某些值做 Reduce,比如说只对 <code>salary>10000</code> 的人做,只选出这个里面的值,它用 Reduce 就可以达到这步,只要传不同的方式给它,你就可以又造出一个新的东西出来。</p>
|
||
<p>说着说着,就到了函数式编程。函数式编程里面,我们可以用很多的像 reduce 这样的函数来完成更多的像 STL 里面的<code>count_if()</code>这样的有具体意义的函数。关于函数式编程,我们会在后面继续具体聊。</p>
|
||
<h1>小结</h1>
|
||
<p>在这篇文章中,我们聊到 C++ 语言是如何通过泛型来解决 C 语言遇到的问题的,其实这里面主要就是泛型编程和函数式编程的基本方法相关的细节,虽然解决编程语言中类型带来的问题可能有多种方式,不一定就是 C++ 这种方式。</p>
|
||
<p>而我之所以从 C/C++ 开始,目的只是因为 C/C++ 都是比较偏底层的编程语言。从底层的原理上,我们可以更透彻地了解,从 C 到 C++ 的演进这一过程中带来的编程方式的变化。这可以让你看到,在静态类型语言方面解决泛型编程的一些技术和方法,从而感受到其中的奥妙和原理。</p>
|
||
<p><strong>因为形式是多样的,但是原理是相通的。所以,这个过程会非常有助于你更深刻地了解后面会谈到的更多的编程范式</strong>。</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/左耳听风/028 编程范式游记(1)- 起源.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/左耳听风/030 编程范式游记(3) - 类型系统和泛型的本质.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099780adfa13cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|