R语言程序包开发

R语言程序包开发:

转载自: 挑圈联靠, 想提高文章的引用率?写个R包吧!系列传送门

01 必备工具

前言

R语言程序包是R语言的灵魂,是R语言的核心,每一个R语言用户都会使用到R包。2006年3月15日,第一个R包(coxrobust)加入CRAN,截止2020年5月17日,已经有超过15000个R包,这些R包涵盖了各个领域,解决了各种各样的问题。

R包的易用性是R广受欢迎的重要原因,R包开发简单、易学,使得各行各业的从业者,即使是非计算机人员,都可以加入到R的编程中来,带来了R繁荣的景象。

既然这么多R包,那还有写包的必要吗?答案是肯定的,现实中的问题千千万万,15000个R包并不能解决所有问题。

如果R包足够实用,确实帮大家解决科研中遇到的统计和可视化问题,大家在应用你的R包的时候自然会引用你介绍R包的文章,自然文章引用量会指数级上涨!

本教程将从零教你如何制作、发布自己的R包。

写包的好处

• 彰显自己的实力!你说你会R,凭啥?有了自己的R包,也是一种实力的体现

• 创造!code changes world!学会写代码,那么你就可实现自己的想法,甚至做出创新的事物

• 软件著作权!你的原创代码可以申请软件著作权,进一步提升自己

• 发表论文!如果你的包是一个创新,可以写论文发表,让更多的人使用

• 完善方法学!不断地修正自己的代码,不断的完善,会加深你对代码和方法学的理解

• 方便自己!即使你不准备发布你的R包,也能方便自己的工作

安装必备工具

制作R包需要的工具有:电脑1台、R软件、RStudio软件、Rtools软件、devtools包、roxygen2包。下面我来详细讲一下安装过程。

1.电脑、R软件、RStudio软件自行安装

  • 电脑的系统可以是windows、macos、linux。

  • R软件及RStudio软件推荐使用最新版本。

2.Rtools软件的安装

许多人误以为Rtools是一个R包,所以会使用install.packages("Rtools")命令来安装,其实是不对的,Rtools其实是windows系统下的软件。

在windows和macos中,Rtools会随着安装RStudio的时候一起安装,如果没有安装,那么windows系统需要自行安装Rtools,macos需要安装Xcode。他们的作用是编译和解析命令。

windows系统Rtools软件的网址是:https://mirrors.tuna.tsinghua.edu.cn/CRAN/ ,根据自己当前使用的R版本,下载对应版本的Rtools软件。比如,我当前使用的R版本是3.6.2,那么我选择下载Rtools35.exe。

img

下载完成后,双击Rtools35.exe进行安装。

img

Rtools并不支持中文,我们可以选择英语,点击OK即可。

img

接下来会告诉你Rtools主要用来创建R语言的应用,对于windows系统来说,需要将其添加入环境变化。

img

安装的默认地址是C:Rtools,建议不要修改。

img

安装的形式,我们直接使用推荐的就可以。

img

最关键的一步:Add rtools to system PATH,一定要勾上,表示加入电脑的环境变量。

img

这里问我们要不要修改环境变量,我们看到C:Rtoolsin已经被加入到了PATH中,我们不需要再次对其进行编辑。

img

再次核对一下安装信息。点击Install,开始安装。

img

开始安装。

img

安装成功!

img

3.安装依赖包

R包的制作依赖于devtools包和roxygen2包,在这两个包出现之前,制作R包是一件十分繁琐的事情,许多事情需要手动完成,一有不慎,就会出错,修改说明文档更加痛苦。

devtools包的出现,尤其是在RStudio软件加入了devtools的功能后,R包制作变得十分简单。

如果说devtools包让制作变得简单,那么roxygen2包彻底解放了R包开始者,我们再也不用为复杂的注释而头疼,可以一边写函数,一边写注释,将精力集中到写代码中,最后运行devtools::document()命令,就可以直接将所有注释写入R的说明文档。

devtools包和roxygen2包的安装命令是

install.packages(c("devtools","roxygen2"))

4. 检查

最后,我们使用命令devtools::has_dev()来检查一下准备工作。运行这行命令,返回“Your system is ready to build packages! ”表示一切准备就绪,可以开始了。

  1. library(devtools)

  2. has_devel()

img

02 创建R包

一节,我们讲了创建R包的准备工具,这一节,开始创建R包!

1. 命名规则

R包的命名主要有以下几个规则:

必须以字母开头(大写或小写),如:ggplot2,不能写成2ggplot

只能包含字母(大写或小写),数字和点,如:covid19.analytics

不能和已有的包名称冲突,已有的包指CRAN和bioconductor上的包

CRAN上已有的包可以在网页https://cran.r-project.org/web/packages/available_packages_by_name.html 里查找,bioconductor上已有的包可以在网页https://bioconductor.org/packages/release/BiocViews.html#___Software 里查找。

也可以使用命令available.packages()查看CRAN上的包,bioManager::available()来查看bioconductor上已有的包。

在创建R包前,不仅需要确保自己的包名是有效的,而且还有起一个好名字。好名字的优点包括:

易记:包的名字不宜全部使用小写字母,可以使用驼峰式如MaN,或aBa,或者可以加入点,如gg.gap

和功能相关:比如做稳健cox回归的包叫做coxrobust

注意版权!已有TCGA是知名网站的名字,那么R包的名字应尽量避免重复,可以加入r,如rTCGA。

不宜太长:很多人会把包的名字起的过长,在CRAN和bioconductor上,大部分R包的名字在4到9个字符之间,过长的包名拼写起来十分困难,无疑增加了使用的难度。

img

****▲**** (上图数据来自CRAN和bioconductor,截止日期是2020-05-18)

2. 创建R包

现在,我们来创建我们的第一个R包(first package):fpkg。

首先,我们使用命令dir.create()在d盘下新建文件夹mypkg,并使用命令setwd()设置工作目录至mypkg文件夹下,以后我们创建的所有R包都在这个目录里面。

  1. dir.create('d:/mypkg')

  2. setwd("d:/mypkg")

接着使用usethis包中的create_package()函数来创建R包,usethis包会随着devtools包的安装而安装,随着devtools包的调用而调用。

  1. library(devtools)

  2. create_package('fpkg')

create_package('fpkg')命令在mypkg文件夹下创建了fpkg文件夹,这个就是我们的R包了,并且自动打开了一个新的界面:fpkg.Rproj,可以直接关闭它。fpkg文件夹(也就是fpkg包)默认创建了以下几个文件:

.Rproj.user文件夹:RStudio的项目文件夹,不需要关注

R文件夹:存放R代码文件夹,**重要*****

• .gitignore文件:存放git信息的文件,会被忽略

DESCRIPTION文件:R包的说明文档,这是整个包的说明文档,**重要*****

fpkg.Rproj文件:RStudio的项目文件,我们以后编辑R包的时候,都是通过打开它来编辑的,因为这个文件包含了创建R包的菜单,**重要****

NAMESPACE文件:存放R包函数命名空间的文件,不需要手动编辑,**极其重要*******

img

create_packages()命令后会自动打开fpkg.Rproj文件,如果不想打开这个文件,可给open参数赋值FALSE,即

create_package('fpkg',open = FALSE)

2. R包中的文件夹

R包中可以包含很多文件夹,不同的文件功能不同,常用的有R文件夹、Data文件夹和vignette文件夹。

• R文件夹是存放代码的地方,也就是我们所有的R代码

• Data文件夹不像R文件夹会自动产生,你可以使用函数use_data()来添加R数据,也可以手动新建,在后面的章节中会详细讲解如何添加数据

• vignette文件夹是用来添加markdown格式的说明文档,可以使用函数use_vignette()来创建,需要注意的是,如果你要添加vignette文件夹,需要安装knitr包、rmarkdowan包和pandoc。前2者的安装可以使用命令install.packages()来安装即可,pandoc的安装参考它的网站https://pandoc.org/installing.html ,后面的章节中会详细讲解vignette的书写。

R包文件的缺陷

很遗憾!R包内的文件夹不支持二级目录,也就是R文件夹下不能再有下一级目录,这个也是R包的缺陷,因为如果函数过多,打理起来会比较麻烦。

小结

今天,创建了我们的第一个R包,它的名字叫fpkg,它是一个空包,并且我们熟悉了R包内的各个文件的作用。


03 封装包

现在,我们已经有了第1个R包:fpkg,为了让大家对制作过程有更加直观的认识,接下来,我们直接封装包。

封装R包主要有2个步骤:写入注释和建包。 有3种实现方式:菜单、命令和快捷键

1.打开项目文件

上一节,我们介绍了R包内的常用文件,有一个特别重要的项目文件fpkg.Rproj,现在,我们双击打开它。

img

和RStudio普通的界面不同,在菜单栏多了可下拉的build菜单项,这里包含了封装R包常用的功能。

img

2.通过菜单封装包

2.1 写入注释

首先是写入注释,在build菜单下,单击Document项。

img

在build窗口中,我们可以看到:更新文档,更新roxygen版本的文档,载入fpkg包,最后完成更新。其实这一步就是将roxygen的注释,转化成R函数的说明文档,如何写roxygen注释我们后面会进一步讲解。

img

2.2 建包

写好注释后,就是建包了,使用build菜单下的Install and Restart或者Clean and Rebuild均可,选择任意一个单击即可,比如我选择Install and Restart

img

选择Install and Restart之后,在build窗口,就可以看到:fpkg包被安装到library文件夹下,最后提示Done (fpkg)表示安装成功。在R的控制中,我们可以看到已经使用library()命令调用了fpkg包,fpkg已经可以使用了。

img

3.使用快捷键封装包

使用快捷键封装包更为简单

• 写入注释:Ctrl/Command + Shift + D,D就是document的意思 • 建包:Ctrl/Command + Shift + B,B就是build的意思

4.使用命令封装包

不论是快捷键还是菜单,本质上都是调用devtools包中的document()函数和build()函数,这并不表示使用命令封装不重要,相反,它非常重要,因为有一些信息仅仅在使用命令的时候才会给出。

• 写入注释:devtools::document() • 建包:devtools::build()

使用命令封装包后的信息,不会再build窗口中显示,而是在控制台中显示。

img

5.调用包

封装完R包后,我们可以使用library()命令来调用我们的包了。

img

没有任何报错信息,说明fpkg包制作成功。

在package窗口中,也可以找到我们的fpkg包。

img

小结

现在我们已经学会如何创建、封装R包了,但是fpkg还只是1个空包,没有任何内容,下一节,我们将开始向包中添加内容---函数。


04 创建函数

函数是R的核心,R是函数的集合。在R中,行使功能的单位就是函数,几个函数捆在一起就成了R包。不管是开发R包,还是学习R,会写函数是必备的技能。

R中有4种常用的函数:标准函数、闭包、中缀操作符和替换函数,也有一些常用但不可编辑的函数,如特殊操作符;还有一些不常用的函数,如非标准计算、元编程。

img

本节内容,我们主要介绍标准函数闭包中缀操作符替换函数

1.标准函数

1.1 标准函数的结构

标准函数由3个部分组成:函数名参数函数体,使用function()函数来创建。在下图中,fun_name表示函数的名字,函数function()括号中的argument表示参数,花括号内的部分就是函数体。

img

1.2 命名规则

标准函数名的命名规则和R包的命名规则类似:

只能包含字母(大写或小写)、数字、点和下划线

• 必须以字母开头

• 不能以点或下划线结尾

一个好的函数名,需要与他的功能相关,如plot(),告诉我们这个一个画图的函数,如sum()是一个求和的函数。函数名最好都是小写字母或数字,这样使用者使用起来更加容易,如果大写、小写、字母、下划线掺杂在一起,拼写起来就十分困难,这并不是一个好的函数名。

比如,我们要创建1个函数sum2,可以使用下面的命令创建

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15424" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(){
}</pre>

1.3 参数

如果想激活一个函数,那么就需要给他传入参数,参数的作用就是给函数传递值。一个函数可以包含有1个或多个参数,也可以没有参数。参数可以传递数值、字符串、向量、数据框、函数、表达式等数据对象。参数在初始时可以为空,也可以带有默认值。

比如,sum2函数的功能是实现2个参数a和b的和

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15429" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(a, b){
sum <- a + b
return(sum)
}</pre>

我们将a和b写在function()函数的内部,表示a和b将会向函数体传递数据。运行这个函数,再来求1和2的和。当我们运行这个函数之后,在全局环境中,就多了一个函数sum2。

img

在sum2(a=1, b=2)中,我们给a赋值1,给b赋值2,将参数名称省略,直接写sum2(1, 2)时,1也会赋值给a,2赋值给b,因为当省略参数名称时,会按照参数位置关系赋值,a在第1位置,b在第2位置,以此类推,不同参数位置之间使用逗号隔开。

img

现在,我们给b赋值默认值1

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15438" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(a, b=1){
sum <- a + b
return(sum)
}</pre>

当b有默认值时,b的赋值将不是必须,如果不给b赋值,将会使用默认的赋值,如果需要改变b的数值,那么可以随意修改。

img

上面所举例的a和b都是任意数值,当参数取值是有限的,比如性别(sex)取值男和女,在构建函数时,最好将所有取值列出,并使用match.arg()进行匹配。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15443" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_sex <- function(sex = c('男', '女')){
sex=match.arg(arg = sex,
choices = c('男','女'))`
return(sex)
}</pre>

在march.arg()(匹配参数)函数中,有2个主要参数,arg(参数)和choices(选项),arg=sex,表示我们要匹配的参数是sex,choices = c('男','女')表示sex只能从男和女中挑选数值。

img

这样做的好处就是当你给sex赋值了男和女以外的数值时,会报错!

img

1.4 三连点

上面的a、b和sex都是有具体名称的参数,但有时候,我们并不需要设置具体的参数,或者说参数的个数可以是无数个,任意多个,那么这个时候就需要使用三连点,可以使用list()函数来获取三连点传递的数值。

我们将sum2函数的功能重新定义为任意多个数据的求和

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15453" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(...){
list = list(...)
sum = do.call(sum,list)
return(sum)
}</pre>

给sum2()函数传递数值,也就是传递给了三连点(...),所有数值都会传递给list()函数,并继续下面的运算。

img

如果想获取三连点输入对象的名称,可以使用do包内的get_names()函数。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15457" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">a=1
return_names <- function(...){
do::get_names(...)
}
return_names(a,b)</pre>

img

1.5 无参数

函数也可以是无参数的,这样函数每次传递出来的数据都是固定的。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15461" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">sum2 <- function(){
sum = 1+2
return(sum)
}</pre>

1.6 return的用法

return()只在函数内起作用,return()的作用是返回数值****,并且中断函数。注意,当运行了第1个return()后,函数的运行就停止,return()后的所有命令都不会运行。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15465" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_test <- function(){
return(1)
return(2)
}</pre>

img

在returntest()函数中,有2个命令,第1个命令是return(1),第2个命令是return(2),分别用来返回1和2。当我们运行returntest()函数时,返回的结果是1,并不是2,表示运行到return(1)这行命令时返回数值,并且停止运行,没有运行return(2)这行命令。

不使用return(),函数也是可以返回数据的,但是仅仅会返回最后1个结果。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15469" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_test <- function(){
1
2
}</pre>

img

删掉return()重新写函数returntest(),运行returntest()返回的数据是2,并没有返回1。

如果想要函数不输出结果,但是可以赋值结果,可以使用invisible()函数。invisible()函数可以使函数返回的结果不打印出来,但是可以赋值给对象。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15473" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">return_test <- function(){
1
invisible(2)
}</pre>

img

image-20200607130821232

小结一下R函数返回结果的逻辑:

返回第1个return()的结果,并结束运算;

否则返回最后1个可以打印的结果;invisible()函数可以隐藏返回的结果(不打印出来),但是可以赋值。

2.闭包

现在,我们已经知道了标准函数的结构:函数名、参数、函数体,闭包区别于标准函数的地方在于闭包没有函数名。也就是说闭包是一个无名函数,仅仅包含了参数和函数体。

闭包的应用场景其实还是很多的,也许你并没有注意到,或者没有闭包这个概念,在做函数测试时,会经常用到闭包;闭包也会被用在函数内部,例如在*apply函数家族中,闭包就是常客。

我们创建1个闭包,功能是求2个数字的和

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15486" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">function(a, b){
a + b
}</pre>

同样我们可以在R中运行这个闭包。

img

当我们运行这个闭包后,没有返回任何错误信息或者警告,而是正常地打印了函数。

如果想返回函数值那就需要在闭包外加圆括号,并且在后面使用圆括号将参数括起来。

(闭包)(参数)

我们使用闭包求1和5的和

img

在*apply家族中使用闭包

img

不管是lapply还是sapply,后面都接了一个无名函数,这个其实就是闭包。

3.中缀函数

中缀函数也叫中缀操作符,它的出现,大大提高了函数的可读性与可操作性,我们以集合运算的set包为例。如果我们要求数据集A和B的交集,与C和D交集的并集,通过set包中的%and%、%or%和圆括号可以非常容易操作,并且易读。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15499" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">A = c(1,2,3,4,5,6)
B = c(1,2,3,4)
C = c(7,8,9)
D = c(8,9)
library(set)
A %and% B %or% (C %and% D)</pre>

中缀函数的结构类似于夹心饼干,两边2个百分号%,中间是它的函数名,函数名不宜过于复杂,那样的话函数拼写起来将十分困难,在set包中,%and%亦可简写为%a%, %or%可简写为%o%, %not%简写为%n%,这是为了使用方便,但是却损失了可读性。

中缀操作符与标准函数的区别在于,****需要使用反引号( ` )或者单引号( ' )或者双引号( " )将“夹心饼干”引起来。我们创建1个%sum%函数,用于求2个数字的和。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="R" cid="n15503" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"># 使用反引号
%sum%<- function(a, b) a + b

或者使用单引号

'%sum%'<- function(a, b) a + b

或者使用双引号

"%sum%"<- function(a, b) a + b</pre>

img

4.替换函数

替换函数就是替换原来的数值,和标准函数、闭包、中缀函数不同,替换函数的功能是直接替换原来的参数,不打印结果。

替换函数同样包含了函数名、参数、函数体3个部分,不同的是,****函数名和参数都是固定的

替换函数的函数名必须是函数名+赋值符号(<-),并且使用单引号( ' )或双引号( " )或反引号( ` )包围,注意函数名和赋值号之间不要有空格,千万不要有空格。替换函数的参数必须是2个,第1个参数名称可以任意,但是第2个参数名称必须是value。

现在,我们要写1个替换函数minus<-,功能是实现a减b,并且直接替换原来的数据a。

普通的做法是

  1. a <- a-b

  2. 我们来构建替换函数minus<-

  3. # 使用反引号,第2个参数必须是value

  4. ``minus<-<- function(x, value){

  5. x - value

  6. }

  7. # 或者,使用单引号,第2个参数必须是value

  8. 'minus<-'<- function(a, value){

  9. a - value

  10. }

  11. # 或者,使用双引号,第2个参数必须是value

  12. "minus<-"<- function(b, value){

  13. b - value

  14. }

我们将a赋值为4,将a减去1,并且替换原来的a

img

我们看到名minus(a)=1运行之后并没有任何输出,而是直接替换了原来的a,当我们再次运行a之后,提示a的值从4变成了3 (即4-1的结果)。

有一个小技巧,在写替换函数的同时,再写一个同样名称的标准函数,例如此处的替换函数minus<-,就可以再写一个同名的标准函数minus(),这样的好处是,不用去区分函数的类型,仅需改变函数的使用方式,就可以行使不同的功能,即同名不同形式。

  1. # 标准函数

  2. minus <- function(x, value){

  3. x - value

  4. }

现在,我们有了2个minus函数,1个是标准函数minus(),另外1个是替换函数minus<-,我们想随时切换a减去1,既可以打印结果,也能替换原来的结果,就像函数colnames,既可以返回数据框或者矩阵的列名称,可以用来替换来原来的列名称,其原因就是有2个同名不同形式的函数:标准函数colnames()和替换函数colnames<-。

img
img

总结

到这里,我们详细讲解了R语言中常用的4种函数:标准函数、闭包、中缀函数和替换函数,详细讲解了函数的结构、写法、参数、函数返回结果的方法,并且解释了对于标准函数和替换函数同名不同形式的使用。


05 环境

环境是R语言中较底层的概念,理解环境可以帮助你进一步理解R的工作原理,对于使用R及R编程都非常有帮助。

1.内容、对象与环境

在电脑中,所有文件保存的位置是文件夹,比如我写了一些“文本文字”,保存在word文档:“环境.docx”中,word文档保存在文件夹“讲稿”下。注意这里的逻辑关系:“文字内容”保存在word文件“环境.docx”里面,而“word”文本(可以和多个)保存在“讲稿”文件夹下。所以,这里的逻辑关系是:

文字内容--->word文本(一个或多个)--->文件夹。

R中的数据存储关系也是这样的。例如,我们将1赋值给a

img

这里的1是我们的“内容”,a是承接内容的载体,a可以承接1,也可以承接2,甚至其他数据内容,a就是我们说的对象,数据对象就是用来承载数据的载体,也就相当于word文本。数字1始终是数字1,而a可以承载1也可以承载其它,所以1我们也叫做常量,a叫做变量。数据对象存储的地方,叫做环境等价于“文件夹”

所以,数据存储在R中的逻辑是:

内容--->对象(一个或多个,相当于word文本)--->环境(相当于文件夹)

数据内容和对象(或者叫做变量),也就是1和a,都是可以看到的。但是环境在R软件中我们是看不到的。在Rstudio软件中,我们可以看到有一个Environment窗口。平时我们查找数据的时候,也是在这个窗口下找寻的,就是因为R中的数据对象保存在环境中。

img

2.局部环境与全局环境

2.1 查看当前活动环境

正如我们可以有很多个文件夹,R也可以多个环境,使用environment()命令来查看当前的环境

img

我们可以看到,当前的活动环境是全局环境(Global Env),既然上面说了对象是保存在环境中,那么我们可以从某一个环境中,提取所有对象。

img

将enviroment()命令的结果赋值给g,可以看到多了一个数据g,g的类型是Environment,我们可以使用美元符号($)来提取g内的对象。

img

2.2 创建新环境并内写入数据

我们可以使用new.env()命令来创建一个新的环境,类似于创建一个新的文件夹。同样也是使用美元符号($)向环境内写入数据。
img

我们使用new.env()命令创建新的环境e之后,环境e的编码是0x000001ed51d25cb0,这个环境编码我们并不需要去理解它,它代表硬盘的一个位置。e$v=9表示向e环境中的v对象写入数字9。

也可以使用assign()命令来给环境赋值,它的命令是assign(对象,内容,环境),我们将对象k保存到环境e中,k的内容是123,同时使用View()命令查看环境e。

img

可以看到,e环境下有2个数据k和v
img

3.局部变量与全局变量 环境是R管理和存储各种数据的地方,每一个函数在创建之初都会创建自己的环境。 我们创建一个函数e1(),用来显示它内部的环境。

img

e1的内部环境编码是0x000001ed50c7f200,相同的命令,我们再次运行1次

img

可以看到即使是完全相同的函数,每次运行时都会创建不同的环境,这是因为电脑的磁盘上并没有它们固定的地方,所以每次运行都在不同的磁盘位置。

注意:我们在前面的步骤中创建了环境e,e内包含了k和v两个数据,它的环境编码是0x000001ed51d25cb0,与此处的编码完全不同。这是因为上面的环境e是在全局环境下创建的,而函数e1()内部的e是在函数内部环境创建的,两个对象虽然都是叫做e,但是所处的环境不同,在磁盘上保存的位置就不同。

现在,我们创建函数e2(),它直接打印e
img

我们看到函数e2内部并没有e,但是却可以打印出e,而且e的内容就是我们前面步骤中创建的环境编码0x000001ed51d25cb0,可见此时的e是函数e2直接引用的全局环境中的e。这是R中环境的另外一个特点:单向:全局环境(父环境)中的数据可以传入它的局部环境(子环境)中,而局部环境中的数据不可以传入全局环境中,如果想将局部环境中的变量传入全局环境中,需要使用全局赋值符号<<-。 我们创建一个函数e3,在函数内部的局部环境中给h赋值78,向全局环境中,添加y,赋值79

img

当我们是用全局赋值符号<<-将79赋值给y后,在全局环境中我们看到了它。

4.全局变量的意义 全局变量的使用并不是很广泛,主要原因是CRAN为避免造成不必要的混乱,禁止函数修改全局变量。 但是全局变量的使用其实是很广泛的,在抓bug时,使用全局变量可以方便我们查看函数的漏洞;在shinyApp制作时,虽然全局变量不能激活响应表达式,但是同样可以传递数值,给shinyApp的制作带来极大便利。

总结

环境是一个晦涩难懂的概念,但是要知道局部环境与全局环境,并且它们是单向的关系,可以使用全局赋值符号将局部环境中的变了传递至全局环境中。

06 函数注释

首先,请允许我赞美一下Hadley和RStudio团队,他们这份工作变得如此简单。

没有roxygen2包的年代,给R函数写注释是一件非常麻烦的事情,工作量极大,并且极易出错,很多时候你需要保持清醒的头脑,因为一不小心就会跳入一个坑,接着便是bug不断,还有debug、debug、debug ……

1.插入注释框架 双击打开我们的项目文件fpkg.Rproj

img

首先,我们写1个标准函数sum2(),用于实现两个数字的和,并将它保存在R文件夹下,文件名为sum2.R,R语言代码文件的扩展名必须是R,而文件名不一定要和函数名一样,可以是sum2.R,也可以是sum.R,但不能是中文。

img

插入注释框架主要有2种方法:菜单插入、快捷键插入,不管哪一种,都要先将光标定位在函数内部,只有在函数内部,RStudio才能识别这是1个函数。

1.1 菜单插入

首先,将光标定位在函数内部,然后依次点击菜单栏的Code、Insert Roxygen Skeleton,就插入了注释框架。

img
img

1.2 快捷键插入

首先,将光标定位在函数内部,然后使用快捷键Ctrl+Alt+Shift+R即可插入。

2.书写注释

普通的注释以 井号# 开头,而函数注释以 井号和单引号 #' 开头,并且后接1个空格。每一个区域的指定,都是以@开头。

2.1 标题

第1行如果不指定,默认是标题title,也可以自己使用@title指定。

img

书写标题时,需要在@title后空1格,标题要能准确反映出函数的功能,要注意首字母的大小写。

img

2.2 描述

函数的描述部分并不是必须的,所以默认是不会插入这1部分的,如果缺省了描述部分,R会自动标题来代替。

使用@description来指定描述区域。描述部分必须是1整个段落,这意味着必须以句号也就是点来结尾。如果内容多,可以换行,每行最好不要超过80个字符,并且换行后要以4个空格开头。

如果description部分写的不过瘾,还可以在@details部分进一步详细书写。

img

2.3 参数

使用@param来指定参数部分,@param后空1格,写参数的名称,再空1格,写参数的解释,参数的解释不能为空。

img

2.4 链接到其它函数

有时候,我们并不想把某一个参数的注释写的过于详细,因为其它地方有非常完备的说明,那么我们就可以使用code{link[pkg]{rdname}}这种形式来引用。

例如,我们将b参数链接到sum函数的说明文档。首先在help窗口中找到sum函数的说明文档

img

在标题上方,可以看到sum{base},告诉我们sum函数的说明文档,处于base包中的sum.rd文档下(rd就是r document的缩写,r说明文档),所以code{link[pkg]{rdname}}中,pkg等于base,rdname等于sum,也就是写成code{link[base]{sum}}

img

2.5 返回

使用@return来说明函数的返回内容。

img

2.6 导出函数

如果你的函数是要分享出来和别人一起使用的,那么就需要将函数从包内导出,在注释部分添加@export即可,后面不需要追加任何内容。

如果你的函数仅仅在包内使用,那么就不需要添加@export,删除即可。

img

2.7 引用函数

如果你想引用其它包中函数,那么需要使用@importFrom,写法是@importFrom+包名+函数名(一个或多个)例如,我们要引用base包中的sum函数,那么就可以这样写

img

如果我们还想引用base包中的abs函数,那么可以分开写两个importFrom

img

也可以写成1行

img

2.8 示例

每个函数都应该有1个示例,这样更加方便用户来理解它。

使用@examples来指定示例部分,例如,我们加入示例sum2(1, 2),那么就可以在@examples后面写上。

img

如果示例不能够被运行,或者运行时间过长,在CRAN检测时不能被通过,可以使用donttest{}来使得示例在检测时自动被忽略。

img

3.转义注释

函数的注释写完了之后,它并不能被R自动识别,需要将其转义才可以。转义的方法我们在第3节《R语言程序包开发(3):封装包》中已经讲解,可以使用快捷键Ctrl+Shift+D,也可以使用命令devtools::document(),还可以使用菜单build、Document,这里我推荐使用devtools::document()。

img

我们可以看到,写入了一个Rd文件,也就R document文件,文件名是sum2.Rd。我们可以打开它看一下。

点击man文件夹下的sum2.Rd文件,可以看到非常复杂的内容,这就是我们最开始写R包时最为痛苦的地方。

img

转义完之后,再次封装包即可。

******小结******

现在,我们已经可以创建包、写函数、写注释、转义注释、封装包了,如果你跟着这个教程走到这里,那么恭喜你,你已经可以完成1个R包。


07 引用R包函数

不同函数可以进行复杂的合作,这是R强大的一点。并不是所有函数都需要你从头写起,借用他人的函数,使你的工作变得更加简单。

本次教程我们将学习

1. 引用CRAN包

2. 引用Bioconductor包

3. Imports、Depends和Suggests的区别

4. 引用函数

****1.引用CRAN包(Imports、Depends和Suggests的区别)****

我们使用usethis包中的usepackage()函数来指定引用包,usepackage()函数中总共有3个参数package、type和minversion****,package表示我们要引用包的名称,type表示引用的类型,常用的引用类型有Imports、Depends和Suggests,minversion表示包的最小版本号。

img

例如,在fpkg包引用do包,那么package就等于"do"。

如果我们只需要使用do包中的join_inner()函数,而不是整个do包,那么引用类型就是Imports,命令如下

img

在返回的文字中,我们看到Adding "do" to Imports field in DESCRIPTION,意思是将do包添加在DESCRIPTION文件中的Imports部分,打开DESCRIPTION文件,可以看到多了Imports部分,而且下面有do。

img

如果我们需要使用整个do包,也就是fpkg包功能的实现完全依赖于do包,而不是仅仅使用某个函数,换句话,当我们library(fpkg)的时候,do包会同时被library进来,也就是同时运行了命令library(do),这种关系就是“依赖”关系(library(我)的时候,也自动library(你)),引用类型就是“依赖”,type等于“Depends”,命令如下

img

返回的文字告诉我们,do被从Imports部分,转移到了Depends部分,并且告诉我们尽量不用使用Depends,Imports是最多且更好的选择。之所以有这样的建议,那是因为当fpkg包对do的关系是Depends时,library(fpkg)时,会同时library(do),这样就增加了函数名称冲突的风险,而当fpkg包对do的关系是Imports,library(fpkg)包的时候,不会运行library(do),也就不会载入do包,从而避免了不必要的函数名冲突,仅仅引用了我们想引用的join_inner()函数。

可以看到,在DESCRIPTION文件中,出现了Depends部分,并且下面有了do。

img

如果fpkg包对do包没有引用和依赖关系,仅仅是在例子中使用了该包,那么我们仅仅是建议安装这个包,来实现我们的例子,这个时候的关系是“建议”,type等于“Suggests”。

所以,DESCRIPTION文件中的Imports、Depends和Suggests分别表示:引用、依赖和建议。

2.引用Bioconductor包

单独使用Imports、Depends和Suggests引用的都是CRAN上的包,如果想引用Bioconductor上的包,需要在前面加上biocViews:,例如,我们想在fpkg包中引用limma包,我们需要将limma写在Imports下面,在Imports前面,加上biocViews:

img

3.引用函数

引用包是引用函数的前提,如果想引用某个函数,必须先引用该函数所属的R包。引用函数有2中形式,通过双引号::直接使用,通过roxygen2注释引用。

例如,我们要引用do包中的join_inner函数,通过双引号来引用的方式为

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n15741" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">do::join_inner()</pre>

通过注释来引用的方式为

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n15746" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">#' @importFrom do join_inner</pre>

引用函数的方式比较简单,但是有一点需要注意:只能引用R包导出的函数,也就是只能引用双引号能够访问的函数。

小结

到这里,我们进一步充实了我们的R包,学会了如何引用包及包内的函数,熟悉了引用包的3种方式,并且学会了如何引用Bioconductor上的包。


08 写DESCRIPTION文件

DESCRIPTION文件是R包的说明文件,使用者通过阅读该文件,可以快速了解你的R包,DESCRIPTION主要包含标题、说明、作者、通讯方式、版本号等几个部分,现在,我来详细说明一下如何书写DESCRIPTION文件。

1.标题

标题一般有字数、内容和大小写的要求

字数不宜过长,一般不要超过20个次,过长的标题不宜阅读

标题要能够体现包的功能

③ 标题的首字母需要大写注意冠词、连词、介词的首字母需要小写

标题的格式至关重要,许多人不在意标题的格式,结果被CRAN要求反复修改。

2.描述

描述部分是一段文字,要以句号结束。描述部分要求详细,详细,详细,重要的事情说3遍啊!描述部分一定要详尽描述包的功能。这部分可以分多个段落来写,另起一段的时候,要以4个空格起写。注意不能以This package或者This function开头,切记切记

******3.作者******

在老版本的R包中,可以直接书写,现在则需要使用person()函数来创建。在person()函数中,主要有几项是需要写的。

img

given:名

family:姓

middle:中间的名字

email:邮件,并不需要每个人的emial,仅给出包的拥有者即可。

role:角色

******4.角色******

每个包可以有多个作者,但是拥有者只能有1个。role的取值如下

aut:作者author

com:编译者compiler

cph:版权拥有者copyright holder

cre:包拥有者creator或者maintainer

ctb:贡献者contributor

ctr:承包者/公司contractor

dtc:贡献数据这data contrbutor

fnd:资助人/组织funder

rev:评论者reviewer

ths:指导者thesis advisor

trl:翻译者translator

5.版本号

R包必须有版本号,每次更新R 包必须有不同的版本号,非正式发布的R包,版本号可以是零点几的版本,例如0.9,正式的R包建议使用一点几以上的版本,R包的版本可以是1.0, 1.01,1.0.0.1。

6.例子

具体的写法以fastStat包为例给大家作为参照。

img

******小结******

通过本次教程的学习,大家学会了如何书写DESCRITION文件,这样就基本可以写一个完整的R包了。如果你刚看到这篇文章,可以戳:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335