├── .gitignore ├── 1_pLNnrvgvmG6Mdi0Yw3mdPQ.png ├── 582e059-411_DevRelations_NodeosGraphic_Option3.png ├── 60539b3-Single-Host-Single-Node-Testnet.png ├── ES-analysis-positionincrement.md ├── LICENSE ├── README.md ├── hello-eos.md ├── kotlin-dsl.md └── kotlin-tips-and-tricks.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /1_pLNnrvgvmG6Mdi0Yw3mdPQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sing1ee/kotlin-road/9a2b0bd6850914dbf0bfbbb45b73c46cbd3ae029/1_pLNnrvgvmG6Mdi0Yw3mdPQ.png -------------------------------------------------------------------------------- /582e059-411_DevRelations_NodeosGraphic_Option3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sing1ee/kotlin-road/9a2b0bd6850914dbf0bfbbb45b73c46cbd3ae029/582e059-411_DevRelations_NodeosGraphic_Option3.png -------------------------------------------------------------------------------- /60539b3-Single-Host-Single-Node-Testnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sing1ee/kotlin-road/9a2b0bd6850914dbf0bfbbb45b73c46cbd3ae029/60539b3-Single-Host-Single-Node-Testnet.png -------------------------------------------------------------------------------- /ES-analysis-positionincrement.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch分词中的PositionIncrement何解? 2 | 3 | 题目中的描述不是十分准确,更多应该是Lucene的分词。Lucene用了很久,其版本更新也很快。在ES出来之后,直接使用Lucene的时候就比较少了,更多的就在ES框架下一站式完成,ES目前在项目中几乎占据了半壁江山。 4 | 5 | ES的功能很强大,使用过程中,有一个问题是绕不过的:就是中文分词。这是至关重要的一个问题,直接影响搜索结果的准确和召回。 6 | 7 | 一般来讲,分词的问题本身目前解决的已经相当不错了,大家用的比较多的是jieba分词,还有清华、斯坦福、复旦等开源的中文分词。如果要在ES中使用jieba分词,就需要定制一个ES的分词插件,将jieba分词load到ES中。 8 | 9 | 几年之前,因为项目需要,我撸过一个简单的ES插件,在github上开源: [jieba分词ES插件](https://github.com/sing1ee/elasticsearch-jieba-plugin),也有一些用户在使用,期间也在断断续续的更新。 10 | 11 | 其中的关键,通过阅读代码就会发现,在处理token的过程中,有以下属性需要处理: 12 | > 13 | - CharTermAttribute 14 | - OffsetAttribute 15 | - TypeAttribute 16 | - PositionIncrementAttribute 17 | 18 | 分别代表了分词的结果的最小单元:term,分词的offset:`startOffset`和`endOffset`,以及词性,例如word、或者数字、字母等等。 19 | 20 | 最后一个属性`PositionIncrementAttribute`比较难以理解,在特定的场合下才需要特殊的处理,大部分情况下默认的结果就可以,但在特定的场合下,会丢掉部分的文档。下文我们就详细解释这个属性,通过例子来说明这个是如何产生影响的,以及该如何解决。 21 | 22 | 我们先解释一下分词的结果,使用到的ES,以及插件版本如下: 23 | 24 | > 25 | - elasticsearch-6.4.0 26 | - elasticsearch-jieba-plugin-6.4.0 27 | 28 | 安装好插件,启动ES: 29 | 30 | ```java 31 | ./bin/elasticsearch 32 | ``` 33 | 有如下输出,则说明插件加载成功: 34 | 35 | ```java 36 | ... 37 | [2018-10-26T23:04:12,572][INFO ][o.e.p.PluginsService ] [z7z-6dR] loaded plugin [analysis-jieba] 38 | ... 39 | ``` 40 | 41 | 准备好示例文档: 42 | 43 | ```java 44 | 现在 高级产品经理\n2003。4-2003。11 产品副经理\n向产品群经理汇报工作\ 负责产品为:得普利麻\n2002。5-2003。3 产品副经理\n向产品群经理汇报工作\n负责推广产品为:精分(思瑞康),麻醉(得普利麻) 45 | ``` 46 | 47 | jieba包括两种分词模式: 48 | 49 | > 50 | - index模式,适用于索引的分词,可以分词更多的term,照顾召回。 51 | - search模式,适用于查询的分词,分词结果没有交叉,更多考虑的是准确率的方面。 52 | 53 | 我们验证一下分词插件,以及两种模式的影响,通过如下命令,我们先看看`search`模式的分词效果: 54 | 55 | ```java 56 | curl -X GET "localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "tokenizer" : "jieba_search", "text" : "现在 高级产品经理\n2003。4-2003。11 产品副经理\n向产品群经理汇报工作\ 负责产品为:得普利麻\n2002。5-2003。3 产品副经理\n向产品群经理汇报工作\n负责推广产品为:精分(思瑞康),麻醉(得普利麻)" }‘ 57 | ``` 58 | 59 | 查看输出: 60 | 61 | ```json 62 | { 63 | "tokens": [ 64 | { 65 | "token": "现在", 66 | "start_offset": 0, 67 | "end_offset": 2, 68 | "type": "word", 69 | "position": 0 70 | }, 71 | { 72 | "token": " ", 73 | "start_offset": 2, 74 | "end_offset": 3, 75 | "type": "word", 76 | "position": 1 77 | }, 78 | { 79 | "token": "高级", 80 | "start_offset": 3, 81 | "end_offset": 5, 82 | "type": "word", 83 | "position": 2 84 | }, 85 | { 86 | "token": "产品", 87 | "start_offset": 5, 88 | "end_offset": 7, 89 | "type": "word", 90 | "position": 3 91 | }, 92 | { 93 | "token": "经理", 94 | "start_offset": 7, 95 | "end_offset": 9, 96 | "type": "word", 97 | "position": 4 98 | }, 99 | { 100 | "token": "\n", 101 | "start_offset": 9, 102 | "end_offset": 10, 103 | "type": "word", 104 | "position": 5 105 | }, 106 | { 107 | "token": "2003", 108 | "start_offset": 10, 109 | "end_offset": 14, 110 | "type": "word", 111 | "position": 6 112 | }, 113 | { 114 | "token": "。", 115 | "start_offset": 14, 116 | "end_offset": 15, 117 | "type": "word", 118 | "position": 7 119 | }, 120 | { 121 | "token": "4", 122 | "start_offset": 15, 123 | "end_offset": 16, 124 | "type": "word", 125 | "position": 8 126 | }, 127 | { 128 | "token": "-", 129 | "start_offset": 16, 130 | "end_offset": 17, 131 | "type": "word", 132 | "position": 9 133 | }, 134 | { 135 | "token": "2003", 136 | "start_offset": 17, 137 | "end_offset": 21, 138 | "type": "word", 139 | "position": 10 140 | }, 141 | { 142 | "token": "。", 143 | "start_offset": 21, 144 | "end_offset": 22, 145 | "type": "word", 146 | "position": 11 147 | }, 148 | { 149 | "token": "11", 150 | "start_offset": 22, 151 | "end_offset": 24, 152 | "type": "word", 153 | "position": 12 154 | }, 155 | { 156 | "token": " ", 157 | "start_offset": 24, 158 | "end_offset": 25, 159 | "type": "word", 160 | "position": 13 161 | }, 162 | { 163 | "token": "产品", 164 | "start_offset": 25, 165 | "end_offset": 27, 166 | "type": "word", 167 | "position": 14 168 | }, 169 | { 170 | "token": "副经理", 171 | "start_offset": 27, 172 | "end_offset": 30, 173 | "type": "word", 174 | "position": 15 175 | }, 176 | { 177 | "token": "\n", 178 | "start_offset": 30, 179 | "end_offset": 31, 180 | "type": "word", 181 | "position": 16 182 | }, 183 | { 184 | "token": "向", 185 | "start_offset": 31, 186 | "end_offset": 32, 187 | "type": "word", 188 | "position": 17 189 | }, 190 | { 191 | "token": "产品", 192 | "start_offset": 32, 193 | "end_offset": 34, 194 | "type": "word", 195 | "position": 18 196 | }, 197 | { 198 | "token": "群", 199 | "start_offset": 34, 200 | "end_offset": 35, 201 | "type": "word", 202 | "position": 19 203 | }, 204 | { 205 | "token": "经理", 206 | "start_offset": 35, 207 | "end_offset": 37, 208 | "type": "word", 209 | "position": 20 210 | }, 211 | { 212 | "token": "汇报工作", 213 | "start_offset": 37, 214 | "end_offset": 41, 215 | "type": "word", 216 | "position": 21 217 | }, 218 | { 219 | "token": "\n", 220 | "start_offset": 41, 221 | "end_offset": 42, 222 | "type": "word", 223 | "position": 22 224 | }, 225 | { 226 | "token": "负责", 227 | "start_offset": 42, 228 | "end_offset": 44, 229 | "type": "word", 230 | "position": 23 231 | }, 232 | { 233 | "token": "产品", 234 | "start_offset": 44, 235 | "end_offset": 46, 236 | "type": "word", 237 | "position": 24 238 | }, 239 | { 240 | "token": "为", 241 | "start_offset": 46, 242 | "end_offset": 47, 243 | "type": "word", 244 | "position": 25 245 | }, 246 | { 247 | "token": ":", 248 | "start_offset": 47, 249 | "end_offset": 48, 250 | "type": "word", 251 | "position": 26 252 | }, 253 | { 254 | "token": "得", 255 | "start_offset": 48, 256 | "end_offset": 49, 257 | "type": "word", 258 | "position": 27 259 | }, 260 | { 261 | "token": "普利", 262 | "start_offset": 49, 263 | "end_offset": 51, 264 | "type": "word", 265 | "position": 28 266 | }, 267 | { 268 | "token": "麻", 269 | "start_offset": 51, 270 | "end_offset": 52, 271 | "type": "word", 272 | "position": 29 273 | }, 274 | { 275 | "token": "\n", 276 | "start_offset": 52, 277 | "end_offset": 53, 278 | "type": "word", 279 | "position": 30 280 | }, 281 | { 282 | "token": "2002", 283 | "start_offset": 53, 284 | "end_offset": 57, 285 | "type": "word", 286 | "position": 31 287 | }, 288 | { 289 | "token": "。", 290 | "start_offset": 57, 291 | "end_offset": 58, 292 | "type": "word", 293 | "position": 32 294 | }, 295 | { 296 | "token": "5", 297 | "start_offset": 58, 298 | "end_offset": 59, 299 | "type": "word", 300 | "position": 33 301 | }, 302 | { 303 | "token": "-", 304 | "start_offset": 59, 305 | "end_offset": 60, 306 | "type": "word", 307 | "position": 34 308 | }, 309 | { 310 | "token": "2003", 311 | "start_offset": 60, 312 | "end_offset": 64, 313 | "type": "word", 314 | "position": 35 315 | }, 316 | { 317 | "token": "。", 318 | "start_offset": 64, 319 | "end_offset": 65, 320 | "type": "word", 321 | "position": 36 322 | }, 323 | { 324 | "token": "3", 325 | "start_offset": 65, 326 | "end_offset": 66, 327 | "type": "word", 328 | "position": 37 329 | }, 330 | { 331 | "token": " ", 332 | "start_offset": 66, 333 | "end_offset": 67, 334 | "type": "word", 335 | "position": 38 336 | }, 337 | { 338 | "token": "产品", 339 | "start_offset": 67, 340 | "end_offset": 69, 341 | "type": "word", 342 | "position": 39 343 | }, 344 | { 345 | "token": "副经理", 346 | "start_offset": 69, 347 | "end_offset": 72, 348 | "type": "word", 349 | "position": 40 350 | }, 351 | { 352 | "token": "\n", 353 | "start_offset": 72, 354 | "end_offset": 73, 355 | "type": "word", 356 | "position": 41 357 | }, 358 | { 359 | "token": "向", 360 | "start_offset": 73, 361 | "end_offset": 74, 362 | "type": "word", 363 | "position": 42 364 | }, 365 | { 366 | "token": "产品", 367 | "start_offset": 74, 368 | "end_offset": 76, 369 | "type": "word", 370 | "position": 43 371 | }, 372 | { 373 | "token": "群", 374 | "start_offset": 76, 375 | "end_offset": 77, 376 | "type": "word", 377 | "position": 44 378 | }, 379 | { 380 | "token": "经理", 381 | "start_offset": 77, 382 | "end_offset": 79, 383 | "type": "word", 384 | "position": 45 385 | }, 386 | { 387 | "token": "汇报工作", 388 | "start_offset": 79, 389 | "end_offset": 83, 390 | "type": "word", 391 | "position": 46 392 | }, 393 | { 394 | "token": "\n", 395 | "start_offset": 83, 396 | "end_offset": 84, 397 | "type": "word", 398 | "position": 47 399 | }, 400 | { 401 | "token": "负责", 402 | "start_offset": 84, 403 | "end_offset": 86, 404 | "type": "word", 405 | "position": 48 406 | }, 407 | { 408 | "token": "推广", 409 | "start_offset": 86, 410 | "end_offset": 88, 411 | "type": "word", 412 | "position": 49 413 | }, 414 | { 415 | "token": "产品", 416 | "start_offset": 88, 417 | "end_offset": 90, 418 | "type": "word", 419 | "position": 50 420 | }, 421 | { 422 | "token": "为", 423 | "start_offset": 90, 424 | "end_offset": 91, 425 | "type": "word", 426 | "position": 51 427 | }, 428 | { 429 | "token": ":", 430 | "start_offset": 91, 431 | "end_offset": 92, 432 | "type": "word", 433 | "position": 52 434 | }, 435 | { 436 | "token": "精分", 437 | "start_offset": 92, 438 | "end_offset": 94, 439 | "type": "word", 440 | "position": 53 441 | }, 442 | { 443 | "token": "(", 444 | "start_offset": 94, 445 | "end_offset": 95, 446 | "type": "word", 447 | "position": 54 448 | }, 449 | { 450 | "token": "思", 451 | "start_offset": 95, 452 | "end_offset": 96, 453 | "type": "word", 454 | "position": 55 455 | }, 456 | { 457 | "token": "瑞康", 458 | "start_offset": 96, 459 | "end_offset": 98, 460 | "type": "word", 461 | "position": 56 462 | }, 463 | { 464 | "token": ")", 465 | "start_offset": 98, 466 | "end_offset": 99, 467 | "type": "word", 468 | "position": 57 469 | }, 470 | { 471 | "token": ",", 472 | "start_offset": 99, 473 | "end_offset": 100, 474 | "type": "word", 475 | "position": 58 476 | }, 477 | { 478 | "token": "麻醉", 479 | "start_offset": 100, 480 | "end_offset": 102, 481 | "type": "word", 482 | "position": 59 483 | }, 484 | { 485 | "token": "(", 486 | "start_offset": 102, 487 | "end_offset": 103, 488 | "type": "word", 489 | "position": 60 490 | }, 491 | { 492 | "token": "得", 493 | "start_offset": 103, 494 | "end_offset": 104, 495 | "type": "word", 496 | "position": 61 497 | }, 498 | { 499 | "token": "普利", 500 | "start_offset": 104, 501 | "end_offset": 106, 502 | "type": "word", 503 | "position": 62 504 | }, 505 | { 506 | "token": "麻", 507 | "start_offset": 106, 508 | "end_offset": 107, 509 | "type": "word", 510 | "position": 63 511 | }, 512 | { 513 | "token": ")", 514 | "start_offset": 107, 515 | "end_offset": 108, 516 | "type": "word", 517 | "position": 64 518 | } 519 | ] 520 | } 521 | ``` 522 | 分词结果中,token对应的就是term属性,start_offset和end_offset对应的就是Offset属性,type类似于词性。这几个都是比较好理解的,那么`position`是什么含义呢?通过观察: 523 | 524 | > `position`是分词之后term/token的先对位置,代表了顺序和距离。 525 | 526 | 这个例子中`产品`和`副经理`是紧挨着的,中间没有间隔。也就意味着如下的查询 527 | 528 | ```json 529 | { 530 | "query": { 531 | "match_phrase":{ 532 | "field1": { 533 | "query": "产品经理", 534 | "slop": 0 535 | } 536 | } 537 | } 538 | } 539 | ``` 540 | 能够搜到我们的示例文档。这里要注意,`slop`默认是0,可以不写。当`slop`要求为0的时候,就要求搜索词组`产品经理`在文档中连起来的,这个时候命中的是`产品经理`,而不是`产品|群|经理`,`|`表示token分割。如果设置`slop`为1,则`产品|群|经理`也会命中。`slop`的大小,就是`position`的大小差异。 541 | 542 | 看下`index`模式,要更加复杂,`PositionIncrement`的作用也是在这里体现。同样是上面的文本: 543 | 544 | ```java 545 | curl -X GET "localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "tokenizer" : "jieba_index", "text" : "现在 高级产品经理\n2003。4-2003。11 产品副经理\n向产品群经理汇报工作\ 负责产品为:得普利麻\n2002。5-2003。3 产品副经理\n向产品群经理汇报工作\n负责推广产品为:精分(思瑞康),麻醉(得普利麻)" }‘ 546 | ``` 547 | 548 | 结果如下,需要仔细对比和`search`的差异。 549 | 550 | ```json 551 | { 552 | "tokens": [ 553 | { 554 | "token": "现在", 555 | "start_offset": 0, 556 | "end_offset": 2, 557 | "type": "word", 558 | "position": 0 559 | }, 560 | { 561 | "token": " ", 562 | "start_offset": 2, 563 | "end_offset": 3, 564 | "type": "word", 565 | "position": 1 566 | }, 567 | { 568 | "token": "高级", 569 | "start_offset": 3, 570 | "end_offset": 5, 571 | "type": "word", 572 | "position": 2 573 | }, 574 | { 575 | "token": "产品", 576 | "start_offset": 5, 577 | "end_offset": 7, 578 | "type": "word", 579 | "position": 3 580 | }, 581 | { 582 | "token": "经理", 583 | "start_offset": 7, 584 | "end_offset": 9, 585 | "type": "word", 586 | "position": 4 587 | }, 588 | { 589 | "token": "\n", 590 | "start_offset": 9, 591 | "end_offset": 10, 592 | "type": "word", 593 | "position": 5 594 | }, 595 | { 596 | "token": "2003", 597 | "start_offset": 10, 598 | "end_offset": 14, 599 | "type": "word", 600 | "position": 6 601 | }, 602 | { 603 | "token": "。", 604 | "start_offset": 14, 605 | "end_offset": 15, 606 | "type": "word", 607 | "position": 7 608 | }, 609 | { 610 | "token": "4", 611 | "start_offset": 15, 612 | "end_offset": 16, 613 | "type": "word", 614 | "position": 8 615 | }, 616 | { 617 | "token": "-", 618 | "start_offset": 16, 619 | "end_offset": 17, 620 | "type": "word", 621 | "position": 9 622 | }, 623 | { 624 | "token": "2003", 625 | "start_offset": 17, 626 | "end_offset": 21, 627 | "type": "word", 628 | "position": 10 629 | }, 630 | { 631 | "token": "。", 632 | "start_offset": 21, 633 | "end_offset": 22, 634 | "type": "word", 635 | "position": 11 636 | }, 637 | { 638 | "token": "11", 639 | "start_offset": 22, 640 | "end_offset": 24, 641 | "type": "word", 642 | "position": 12 643 | }, 644 | { 645 | "token": " ", 646 | "start_offset": 24, 647 | "end_offset": 25, 648 | "type": "word", 649 | "position": 13 650 | }, 651 | { 652 | "token": "产品", 653 | "start_offset": 25, 654 | "end_offset": 27, 655 | "type": "word", 656 | "position": 14 657 | }, 658 | { 659 | "token": "副经理", 660 | "start_offset": 27, 661 | "end_offset": 30, 662 | "type": "word", 663 | "position": 15 664 | }, 665 | { 666 | "token": "经理", 667 | "start_offset": 28, 668 | "end_offset": 30, 669 | "type": "word", 670 | "position": 16 671 | }, 672 | { 673 | "token": "\n", 674 | "start_offset": 30, 675 | "end_offset": 31, 676 | "type": "word", 677 | "position": 17 678 | }, 679 | { 680 | "token": "向", 681 | "start_offset": 31, 682 | "end_offset": 32, 683 | "type": "word", 684 | "position": 18 685 | }, 686 | { 687 | "token": "产品", 688 | "start_offset": 32, 689 | "end_offset": 34, 690 | "type": "word", 691 | "position": 19 692 | }, 693 | { 694 | "token": "群", 695 | "start_offset": 34, 696 | "end_offset": 35, 697 | "type": "word", 698 | "position": 20 699 | }, 700 | { 701 | "token": "经理", 702 | "start_offset": 35, 703 | "end_offset": 37, 704 | "type": "word", 705 | "position": 21 706 | }, 707 | { 708 | "token": "汇报", 709 | "start_offset": 37, 710 | "end_offset": 39, 711 | "type": "word", 712 | "position": 22 713 | }, 714 | { 715 | "token": "汇报工作", 716 | "start_offset": 37, 717 | "end_offset": 41, 718 | "type": "word", 719 | "position": 22 720 | }, 721 | { 722 | "token": "工作", 723 | "start_offset": 39, 724 | "end_offset": 41, 725 | "type": "word", 726 | "position": 23 727 | }, 728 | { 729 | "token": "\n", 730 | "start_offset": 41, 731 | "end_offset": 42, 732 | "type": "word", 733 | "position": 24 734 | }, 735 | { 736 | "token": "负责", 737 | "start_offset": 42, 738 | "end_offset": 44, 739 | "type": "word", 740 | "position": 25 741 | }, 742 | { 743 | "token": "产品", 744 | "start_offset": 44, 745 | "end_offset": 46, 746 | "type": "word", 747 | "position": 26 748 | }, 749 | { 750 | "token": "为", 751 | "start_offset": 46, 752 | "end_offset": 47, 753 | "type": "word", 754 | "position": 27 755 | }, 756 | { 757 | "token": ":", 758 | "start_offset": 47, 759 | "end_offset": 48, 760 | "type": "word", 761 | "position": 28 762 | }, 763 | { 764 | "token": "得", 765 | "start_offset": 48, 766 | "end_offset": 49, 767 | "type": "word", 768 | "position": 29 769 | }, 770 | { 771 | "token": "普利", 772 | "start_offset": 49, 773 | "end_offset": 51, 774 | "type": "word", 775 | "position": 30 776 | }, 777 | { 778 | "token": "麻", 779 | "start_offset": 51, 780 | "end_offset": 52, 781 | "type": "word", 782 | "position": 31 783 | }, 784 | { 785 | "token": "\n", 786 | "start_offset": 52, 787 | "end_offset": 53, 788 | "type": "word", 789 | "position": 32 790 | }, 791 | { 792 | "token": "2002", 793 | "start_offset": 53, 794 | "end_offset": 57, 795 | "type": "word", 796 | "position": 33 797 | }, 798 | { 799 | "token": "。", 800 | "start_offset": 57, 801 | "end_offset": 58, 802 | "type": "word", 803 | "position": 34 804 | }, 805 | { 806 | "token": "5", 807 | "start_offset": 58, 808 | "end_offset": 59, 809 | "type": "word", 810 | "position": 35 811 | }, 812 | { 813 | "token": "-", 814 | "start_offset": 59, 815 | "end_offset": 60, 816 | "type": "word", 817 | "position": 36 818 | }, 819 | { 820 | "token": "2003", 821 | "start_offset": 60, 822 | "end_offset": 64, 823 | "type": "word", 824 | "position": 37 825 | }, 826 | { 827 | "token": "。", 828 | "start_offset": 64, 829 | "end_offset": 65, 830 | "type": "word", 831 | "position": 38 832 | }, 833 | { 834 | "token": "3", 835 | "start_offset": 65, 836 | "end_offset": 66, 837 | "type": "word", 838 | "position": 39 839 | }, 840 | { 841 | "token": " ", 842 | "start_offset": 66, 843 | "end_offset": 67, 844 | "type": "word", 845 | "position": 40 846 | }, 847 | { 848 | "token": "产品", 849 | "start_offset": 67, 850 | "end_offset": 69, 851 | "type": "word", 852 | "position": 41 853 | }, 854 | { 855 | "token": "副经理", 856 | "start_offset": 69, 857 | "end_offset": 72, 858 | "type": "word", 859 | "position": 42 860 | }, 861 | { 862 | "token": "经理", 863 | "start_offset": 70, 864 | "end_offset": 72, 865 | "type": "word", 866 | "position": 43 867 | }, 868 | { 869 | "token": "\n", 870 | "start_offset": 72, 871 | "end_offset": 73, 872 | "type": "word", 873 | "position": 44 874 | }, 875 | { 876 | "token": "向", 877 | "start_offset": 73, 878 | "end_offset": 74, 879 | "type": "word", 880 | "position": 45 881 | }, 882 | { 883 | "token": "产品", 884 | "start_offset": 74, 885 | "end_offset": 76, 886 | "type": "word", 887 | "position": 46 888 | }, 889 | { 890 | "token": "群", 891 | "start_offset": 76, 892 | "end_offset": 77, 893 | "type": "word", 894 | "position": 47 895 | }, 896 | { 897 | "token": "经理", 898 | "start_offset": 77, 899 | "end_offset": 79, 900 | "type": "word", 901 | "position": 48 902 | }, 903 | { 904 | "token": "汇报", 905 | "start_offset": 79, 906 | "end_offset": 81, 907 | "type": "word", 908 | "position": 49 909 | }, 910 | { 911 | "token": "汇报工作", 912 | "start_offset": 79, 913 | "end_offset": 83, 914 | "type": "word", 915 | "position": 49 916 | }, 917 | { 918 | "token": "工作", 919 | "start_offset": 81, 920 | "end_offset": 83, 921 | "type": "word", 922 | "position": 50 923 | }, 924 | { 925 | "token": "\n", 926 | "start_offset": 83, 927 | "end_offset": 84, 928 | "type": "word", 929 | "position": 51 930 | }, 931 | { 932 | "token": "负责", 933 | "start_offset": 84, 934 | "end_offset": 86, 935 | "type": "word", 936 | "position": 52 937 | }, 938 | { 939 | "token": "推广", 940 | "start_offset": 86, 941 | "end_offset": 88, 942 | "type": "word", 943 | "position": 53 944 | }, 945 | { 946 | "token": "产品", 947 | "start_offset": 88, 948 | "end_offset": 90, 949 | "type": "word", 950 | "position": 54 951 | }, 952 | { 953 | "token": "为", 954 | "start_offset": 90, 955 | "end_offset": 91, 956 | "type": "word", 957 | "position": 55 958 | }, 959 | { 960 | "token": ":", 961 | "start_offset": 91, 962 | "end_offset": 92, 963 | "type": "word", 964 | "position": 56 965 | }, 966 | { 967 | "token": "精分", 968 | "start_offset": 92, 969 | "end_offset": 94, 970 | "type": "word", 971 | "position": 57 972 | }, 973 | { 974 | "token": "(", 975 | "start_offset": 94, 976 | "end_offset": 95, 977 | "type": "word", 978 | "position": 58 979 | }, 980 | { 981 | "token": "思", 982 | "start_offset": 95, 983 | "end_offset": 96, 984 | "type": "word", 985 | "position": 59 986 | }, 987 | { 988 | "token": "瑞康", 989 | "start_offset": 96, 990 | "end_offset": 98, 991 | "type": "word", 992 | "position": 60 993 | }, 994 | { 995 | "token": ")", 996 | "start_offset": 98, 997 | "end_offset": 99, 998 | "type": "word", 999 | "position": 61 1000 | }, 1001 | { 1002 | "token": ",", 1003 | "start_offset": 99, 1004 | "end_offset": 100, 1005 | "type": "word", 1006 | "position": 62 1007 | }, 1008 | { 1009 | "token": "麻醉", 1010 | "start_offset": 100, 1011 | "end_offset": 102, 1012 | "type": "word", 1013 | "position": 63 1014 | }, 1015 | { 1016 | "token": "(", 1017 | "start_offset": 102, 1018 | "end_offset": 103, 1019 | "type": "word", 1020 | "position": 64 1021 | }, 1022 | { 1023 | "token": "得", 1024 | "start_offset": 103, 1025 | "end_offset": 104, 1026 | "type": "word", 1027 | "position": 65 1028 | }, 1029 | { 1030 | "token": "普利", 1031 | "start_offset": 104, 1032 | "end_offset": 106, 1033 | "type": "word", 1034 | "position": 66 1035 | }, 1036 | { 1037 | "token": "麻", 1038 | "start_offset": 106, 1039 | "end_offset": 107, 1040 | "type": "word", 1041 | "position": 67 1042 | }, 1043 | { 1044 | "token": ")", 1045 | "start_offset": 107, 1046 | "end_offset": 108, 1047 | "type": "word", 1048 | "position": 68 1049 | } 1050 | ] 1051 | } 1052 | ``` 1053 | 因为`index`模式的原因,`产品副经理`分为了`产品|副经理|经理`。这个时候,合理的`position`就十分重要了。通过我最新的插件的实现,这里的`position`分别是14,15,16。这是正确的,因为要正确处理下面的结果。 1054 | 1055 | 当我们执行如下搜索: 1056 | 1057 | ```json 1058 | { 1059 | "query": { 1060 | "match_phrase":{ 1061 | "field1": { 1062 | "query": "产品经理" 1063 | } 1064 | } 1065 | }, 1066 | "highlight" : { 1067 | "fields" : { 1068 | "field1" : {} 1069 | } 1070 | } 1071 | } 1072 | ``` 1073 | 命中我们的示例文本,无间隔的`产品经理`可以命中,并且可以高亮,但是`产品副经理`没有命中,也没有高亮。 1074 | 1075 | 再看这个例子: 1076 | 1077 | ```json 1078 | { 1079 | "query": { 1080 | "match_phrase":{ 1081 | "field1": { 1082 | "query": "产品经理", 1083 | "slop": 2 1084 | } 1085 | } 1086 | }, 1087 | "highlight" : { 1088 | "fields" : { 1089 | "field1" : {} 1090 | } 1091 | } 1092 | } 1093 | ``` 1094 | 则,无间隔的`产品经理`可以命中,并且可以高亮;同时,`产品副经理`有命中,`产品`和`经理`分别高亮。这两个例子的差别,大家要细细体会。 1095 | 1096 | 那么如何正确的处理`position`呢,关键就在于`PositionIncrementAttribute`属性的处理,通常我们使用`search`模式类似的分词是不会遇到问题的,即使使用默认的`PositionIncrementAttribute`的实现:根据分词得到的token,每次`+1`,从而得到`position`。 1097 | 1098 | 但默认的实现,遇到如下的情况,就会出现问题: 1099 | 1100 | 示例文本: 1101 | 1102 | ```java 1103 | 中国人民解放军胜利了。 1104 | ``` 1105 | 1106 | 如果采用默认的实现,则输出: 1107 | 1108 | ```json 1109 | { 1110 | "tokens": [ 1111 | { 1112 | "token": "中国", 1113 | "start_offset": 0, 1114 | "end_offset": 2, 1115 | "type": "word", 1116 | "position": 0 1117 | }, 1118 | { 1119 | "token": "中国人", 1120 | "start_offset": 0, 1121 | "end_offset": 3, 1122 | "type": "word", 1123 | "position": 1 1124 | }, 1125 | { 1126 | "token": "中国人民解放军", 1127 | "start_offset": 0, 1128 | "end_offset": 7, 1129 | "type": "word", 1130 | "position": 2 1131 | }, 1132 | { 1133 | "token": "国人", 1134 | "start_offset": 1, 1135 | "end_offset": 3, 1136 | "type": "word", 1137 | "position": 4 1138 | }, 1139 | { 1140 | "token": "人民", 1141 | "start_offset": 2, 1142 | "end_offset": 4, 1143 | "type": "word", 1144 | "position": 5 1145 | }, 1146 | { 1147 | "token": "解放", 1148 | "start_offset": 4, 1149 | "end_offset": 6, 1150 | "type": "word", 1151 | "position": 6 1152 | }, 1153 | { 1154 | "token": "解放军", 1155 | "start_offset": 4, 1156 | "end_offset": 7, 1157 | "type": "word", 1158 | "position": 7 1159 | }, 1160 | { 1161 | "token": "胜利", 1162 | "start_offset": 7, 1163 | "end_offset": 9, 1164 | "type": "word", 1165 | "position": 8 1166 | }, 1167 | { 1168 | "token": "了", 1169 | "start_offset": 9, 1170 | "end_offset": 10, 1171 | "type": "word", 1172 | "position": 9 1173 | } 1174 | ] 1175 | } 1176 | ``` 1177 | 根据这样的`position`,我们如下的查询,就找不到这个示例文档,从而产生丢数据的现象。 1178 | 1179 | ```json 1180 | { 1181 | "query": { 1182 | "match_phrase":{ 1183 | "field1": { 1184 | "query": "中国人民" 1185 | } 1186 | } 1187 | }, 1188 | "highlight" : { 1189 | "fields" : { 1190 | "field1" : {} 1191 | } 1192 | } 1193 | } 1194 | ``` 1195 | 1196 | 本来`中国人民`在示例中是无间隔紧邻的,但是由于`position`解析的问题,直接导致`slop`已经变成了4,所以必须制定查询中的`slop`比较大,才能够返回正确的文档,但这里Rank也会受到影响。 1197 | 1198 | 看一下正确`position`的结果。 1199 | 1200 | ```json 1201 | { 1202 | "tokens": [ 1203 | { 1204 | "token": "中国", 1205 | "start_offset": 0, 1206 | "end_offset": 2, 1207 | "type": "word", 1208 | "position": 0 1209 | }, 1210 | { 1211 | "token": "中国人", 1212 | "start_offset": 0, 1213 | "end_offset": 3, 1214 | "type": "word", 1215 | "position": 0 1216 | }, 1217 | { 1218 | "token": "中国人民解放军", 1219 | "start_offset": 0, 1220 | "end_offset": 7, 1221 | "type": "word", 1222 | "position": 0 1223 | }, 1224 | { 1225 | "token": "国人", 1226 | "start_offset": 1, 1227 | "end_offset": 3, 1228 | "type": "word", 1229 | "position": 0 1230 | }, 1231 | { 1232 | "token": "人民", 1233 | "start_offset": 2, 1234 | "end_offset": 4, 1235 | "type": "word", 1236 | "position": 1 1237 | }, 1238 | { 1239 | "token": "解放", 1240 | "start_offset": 4, 1241 | "end_offset": 6, 1242 | "type": "word", 1243 | "position": 2 1244 | }, 1245 | { 1246 | "token": "解放军", 1247 | "start_offset": 4, 1248 | "end_offset": 7, 1249 | "type": "word", 1250 | "position": 2 1251 | }, 1252 | { 1253 | "token": "胜利", 1254 | "start_offset": 7, 1255 | "end_offset": 9, 1256 | "type": "word", 1257 | "position": 3 1258 | }, 1259 | { 1260 | "token": "了", 1261 | "start_offset": 9, 1262 | "end_offset": 10, 1263 | "type": "word", 1264 | "position": 4 1265 | } 1266 | ] 1267 | } 1268 | ``` 1269 | 其中,`中国`是0,`人民`是1,就可以命中了。 1270 | 1271 | 基本上,在处理token的时候,要判断``是1,还是0。这里的Lucene实现机制不好,对于分词的实现约束比较多,并且只考虑了英文。现在的实现,优先考虑了召回。极个别情况,还是会有些准确率的问题。 1272 | 1273 | 另外一个层面,要从词的切分的角度处理,分词的结果应该提供一个最细粒度的、无交叉的切分,这个方式用来做索引,会比较好一些。那这样,默认的`PositionIncrement`也是能够满足需求的。接下来看看,`jieba`是否可以改造一下,支持第三种分词的模式:最细粒度的、无交叉的切分。 1274 | 1275 | 1276 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cheng Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The road to Rome 2 | 3 | 1. [kotlin tips and tricks](kotlin-tips-and-tricks.md) 4 | 1. [Hello EOS](hello-eos.md) 5 | 1. [ES分词的PositionIncrement作用解析](ES-analysis-positionincrement.md) 6 | 1. [Kotlin DSL](kotlin-dsl.md) 7 | -------------------------------------------------------------------------------- /hello-eos.md: -------------------------------------------------------------------------------- 1 | # 非典型EOS开发入门 2 | 3 | #### 非典型 4 | EOS上线稳定运行一段时间了,最近也是成为众多博彩的DAPP首选公链。但我们这篇文章不讲区块链,也不讲去中心化,也不讲币,只从技术的角度,从使用一个类似SDK的工具的角度,来一个入门之旅。毕竟EOS蛮成熟了。 5 | 6 | > 注:以下操作都在`Mac`上进行 7 | 8 | #### 安装 9 | 10 | 安装方法有比较简单,和半年前不可同日而语,那时候编译通过也是比较困难的事情。 11 | 首先通过`brew`安装。 12 | 13 | ```cpp 14 | // install 15 | brew tap eosio/eosio 16 | brew install eosio 17 | // uninstall 18 | brew remove eosio 19 | ``` 20 | 21 | 安装这里,用`brew`是一种快捷的方法,但是对学习无益。而且,对于`brew`的安装,似乎安装的不完整,我没有找到`eosiocpp`等命令。从源码`build`,是有的。过程略复杂,依赖比较多,需要配置一些环境变量,大家有问题可以留言讨论,Github里提一个`issue`,也OK。 22 | 23 | #### 命令解析与基本操作 24 | 25 | EOS安装完毕主要有三个命令: 26 | 27 | - `nodeos`是节点程序,一系列可配置的插件,包括生产区块的功能,下文有介绍,可以用来作为本地的开发测试环境 28 | - `cleos`负责和区块链以及钱包,进行命令行方式的交互 29 | - `keosd`用来安全的存储钱包中的keys 30 | 31 | 三者之间的关系如下: 32 | ![582e059-411_DevRelations_NodeosGraphic_Option3](582e059-411_DevRelations_NodeosGraphic_Option3.png) 33 | 34 | #### 单节点测试网络 35 | 36 | 本地开发需要一个环境,在安装好eos的基础之上,我们运行单节点的测试网络。 37 | 38 | 运行如下命令: 39 | 40 | ```cpp 41 | nodeos -e -p eosio --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin 42 | ``` 43 | 很快就会有区块产生,大部分的交易数都是0: 44 | 45 | ```cpp 46 | info 2018-10-25T13:38:25.505 thread-0 producer_plugin.cpp:1490 produce_block ] Produced block 0000a44aaf490a1d... #42058 @ 2018-10-25T13:38:25.500 signed by eosio [trxs: 0, lib: 42057, confirmed: 0] 47 | info 2018-10-25T13:38:26.003 thread-0 producer_plugin.cpp:1490 produce_block ] Produced block 0000a44b95765523... #42059 @ 2018-10-25T13:38:26.000 signed by eosio [trxs: 0, lib: 42058, confirmed: 0] 48 | info 2018-10-25T13:38:26.502 thread-0 producer_plugin.cpp:1490 produce_block ] Produced block 0000a44c3473bb8f... #42060 @ 2018-10-25T13:38:26.500 signed by eosio [trxs: 0, lib: 42059, confirmed: 0] 49 | info 2018-10-25T13:38:27.000 thread-0 producer_plugin.cpp:1490 produce_block ] Produced block 0000a44de15a88ef... #42061 @ 2018-10-25T13:38:27.000 signed by eosio [trxs: 0, lib: 42060, confirmed: 0] 50 | info 2018-10-25T13:38:27.504 thread-0 producer_plugin.cpp:1490 produce_block ] Produced block 0000a44e31032a7f... #42062 @ 2018-10-25T13:38:27.500 signed by eosio [trxs: 0, lib: 42061, confirmed: 0] 51 | ``` 52 | 如上输出,则意味着节点启动成功,运行着一个叫做`eosio`的区块生产者。下图展示的是这个单节点是如何工作的。 53 | 54 | 55 | ![60539b3-Single-Host-Single-Node-Testnet](60539b3-Single-Host-Single-Node-Testnet.png) 56 | 57 | 启动之后,默认的配置文件存放位置为: 58 | 59 | 1. Mac 60 | 61 | ```cpp 62 | ~/Library/Application\ Support/eosio/nodeos/config 63 | ``` 64 | 2. Linux 65 | 66 | ```cpp 67 | ~/.local/share/eosio/nodeos/config 68 | ``` 69 | 同时,我们也可以使用`--config-dir`,在命令行执行配置目录,但要注意,当通过命令行制定,需要将`genesis.json`文件放到自定义的配置目录中。 70 | 71 | 同样,也可以制定数据目录,如下: 72 | 73 | 1. Mac 74 | 75 | ```cpp 76 | ~/Library/Application\ Support/eosio/nodeos/data 77 | ``` 78 | 2. Linux 79 | 80 | ```cpp 81 | ~/.local/share/eosio/nodeos/data 82 | ``` 83 | 也可以通过命令行来指定,参数为:`--data-dir`。 84 | 85 | 以上,节点启动成功,可以通过如下的命令检查: 86 | 87 | ```cpp 88 | curl --request POST --url http://127.0.0.1:8888/v1/chain/get_info | python -m json.tool 89 | ``` 90 | 输出为: 91 | 92 | ```cpp 93 | { 94 | "block_cpu_limit": 199900, 95 | "block_net_limit": 1048576, 96 | "chain_id": "cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f", 97 | "head_block_id": "0000a82fa75500a7d2bc8be4d45c108c4e503600367259f349a54699c08eb2d0", 98 | "head_block_num": 43055, 99 | "head_block_producer": "eosio", 100 | "head_block_time": "2018-10-25T13:46:44.000", 101 | "last_irreversible_block_id": "0000a82e536d530d8d7e8912ff996bc42ee7dd1d3f8b5e13bfa2a1773d52bef5", 102 | "last_irreversible_block_num": 43054, 103 | "server_version": "f9a3d023", 104 | "server_version_string": "v1.4.1-dirty", 105 | "virtual_block_cpu_limit": 200000000, 106 | "virtual_block_net_limit": 1048576000 107 | } 108 | ``` 109 | 上面清晰的可以看到节点配置的详情信息,如`server_version_string`。更多的API可以查看官方的文档。 110 | 111 | #### 为智能合约准备 112 | 113 | 在EOS上,智能合约采用的是`WebAssembly`格式代码,可由C++, Rust, Python, Kotlin等编写编译生成,但目前C++的支持很完善。在使用C++编写完成合约代码后,通过EOSIO软件中提供的eosiocpp工具,将C++代码编译生成WASM(wasm的文本格式是后缀是wast,下文可以看到)文件和abi文件,再利用cleos工具将代码部署到链上,也就是存到块数据中。 114 | 115 | 特别说明几个概念: 116 | 117 | - `abi` 应用程序二进制接口(ABI)是一个基于JSON的描述,介绍如何在JSON和二进制表示之间转换用户操作。ABI还描述了如何将数据库状态转换为JSON或从JSON转换。文件格式类似JSON,用来定义智能合约与EOS系统外部交互的数据接口。将cpp编译为abi: 118 | 119 | ```cpp 120 | eosiocpp -g ${contract}.abi ${contract}.hpp 121 | ``` 122 | - `wast` 任何要部署到EOSIO区块链的程序都必须编译成WASM格式。这是区块链接受的唯一格式。`.wast`是`.wasm`的文本格式;将cpp编译为WASM: 123 | 124 | ```cpp 125 | eosiocpp -o ${contract}.wast ${contract}.cpp 126 | ``` 127 | 128 | 智能合约名即账号名,在上述部署合约(下面Hello EOS会介绍如何部署和测试)时就已经绑定了账号。在满足条件或被调用时,超级节点就会执行相关合约,并将执行结果的数据更新到内存数据库中,同时也会更新到块数据中。 129 | 130 | 与以太坊简单对比: 131 | 132 | 1. 合约名称与合约地址的差异:以太坊合约通过地址区分,EOS的合约名就是账号名。 133 | 2. 合约更新的差异。如果以太坊合约要更新,就是一个新的地址,所以以太坊才是真正的`智能合约`,EOS是通过账户名区分,智能合约和普通的应用类型一样,可以更新。所以这个点上,`EOS更多的是给应用提供了可靠遍历的支付方式`。我想这一点很重要。 134 | 3. 资源消耗,以太坊消耗GAS,每个操作都有手续费。EOS智能合约不需要手续费,但部署合约需要RAM,发送消息和执行合约需要消耗抵押获得的CPU和网络带宽。 135 | 136 | 接下来就动手,来一个Hello World。 137 | 138 | #### Hello EOS 139 | 140 | 开始编写EOS智能合约,EOS智能合约是采用C++语言编写,是项目方综合考虑安全和效率。 141 | 142 | 编写智能合约可以通过命令生成模板: 143 | 144 | ```cpp 145 | $ eosiocpp -n hello 146 | ``` 147 | 当然,现阶段可以直接copy如下的代码: 148 | 149 | ```cpp 150 | #include 151 | #include 152 | using namespace eosio; 153 | 154 | class hello : public eosio::contract { 155 | public: 156 | using contract::contract; 157 | /// @abi action 158 | void hi( account_name user ) { 159 | print( "Hello, World", name{user} ); 160 | } 161 | }; 162 | EOSIO_ABI( hello, (hi) ) 163 | ``` 164 | 简单说明一下代码: 165 | 166 | ```cpp 167 | #include 168 | #include 169 | ``` 170 | 引入EOS智能合约的头文件。 171 | 172 | ```cpp 173 | using namespace eosio; 174 | ``` 175 | 使用默认的命名空间,可以自定义。 176 | 177 | ```cpp 178 | class hello:public eosio :: contract { 179 | ``` 180 | EOS中,智能合约都要继承`eosio :: contract`。 181 | 182 | 183 | 编译EOS智能合约 184 | 185 | ```cpp 186 | eosiocpp -o hello.wast hello.cpp 187 | eosiocpp -g hello.abi hello.cpp 188 | ``` 189 | 190 | 部署合约需要创建测试用的钱包、密钥和账户。 191 | 192 | EOS安装启动之后,默认有一个default钱包,我们也可以通过如下命令创建 193 | 194 | ```cpp 195 | cleos wallet create -n <钱包名字> 196 | ``` 197 | 创建钱包的同时,会生成一个密码,请妥善保存,在对钱包进行一些操作的时候,需要这个密码,例如解锁钱包。我们的演示里,直接使用default钱包。 198 | 199 | 在以下的过程中,如果提示钱包已经锁定,则执行解锁的命令: 200 | 201 | ```cpp 202 | cleos wallet unlock -n default 203 | ``` 204 | 解锁名字为`default`的钱包 205 | 206 | 接下来用`cleos`创建一个密钥对: 207 | 208 | ```cpp 209 | cleos create key 210 | ``` 211 | 保存好private key和public key,后面创建账号的时候,需要用。 212 | 213 | 在创建账号之前,需要将密钥导入到钱包中: 214 | 215 | ```cpp 216 | cleos wallet import -n scuser --private-key 217 | ``` 218 | 219 | 我们需要两个密钥对,重复上面的两个步骤,再生成一个密钥对,然后导入钱包。 220 | 221 | 此时通过如下命令: 222 | 223 | ```cpp 224 | cleos wallet keys 225 | ``` 226 | 可以看到刚导入的两个密钥对,展示的是public key: 227 | 228 | ```cpp 229 | [ 230 | "EOS62d7M3N7ZkY5gMFhGKygenKNnrAtuGrvjkv9ap3Py1seLo9DYh", 231 | "EOS8B2wVFnPk1TNTVLjdKgtiLMtv8r8KdHKWj3U7zciUvMyo36bnT" 232 | ] 233 | ``` 234 | 大家这里看到的具体的public key,与我不同。 235 | 236 | 下面要创建账号,命令如下: 237 | 238 | ```cpp 239 | $ cleos create account eosio myuser EOS8B2wVFnPk1TNTVLjdKgtiLMtv8r8KdHKWj3U7zciUvMyo36bnT EOS62d7M3N7ZkY5gMFhGKygenKNnrAtuGrvjkv9ap3Py1seLo9DYh 240 | Error 3090003: Provided keys, permissions, and delays do not satisfy declared authorizations 241 | Ensure that you have the related private keys inside your wallet and your wallet is unlocked. 242 | ``` 243 | 出现问题,按照如下方法解决: 244 | 245 | 1. 打开配置文件 246 | 1. Mac 247 | 248 | ```cpp 249 | ~/Library/Application\ Support/eosio/nodeos/config 250 | ``` 251 | 2. Linux 252 | 253 | ```cpp 254 | ~/.local/share/eosio/nodeos/config 255 | ``` 256 | 2. 找到signature-provider的私钥 257 | 3. 将私钥导入到钱包中 258 | 259 | 再次尝试成功,结果如下 260 | 261 | ```cpp 262 | executed transaction: 199fcf44e40602dff76d8231b5f11129f0fda9c15fef3b684ef4149e032bab79 200 bytes 904 us 263 | # eosio <= eosio::newaccount {"creator":"eosio","name":"myuser","owner":{"threshold":1,"keys":[{"key":"EOS8B2wVFnPk1TNTVLjdKgtiLM... 264 | ``` 265 | 266 | 这时执行`cleos wallet keys`,就会发现,多了一个public key: 267 | 268 | ```cpp 269 | [ 270 | "EOS62d7M3N7ZkY5gMFhGKygenKNnrAtuGrvjkv9ap3Py1seLo9DYh", 271 | "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", 272 | "EOS8B2wVFnPk1TNTVLjdKgtiLMtv8r8KdHKWj3U7zciUvMyo36bnT" 273 | ] 274 | ``` 275 | 276 | 智能合约成功发布到本地测试网络 277 | 278 | ```cpp 279 | $ cleos set contract myuser ~/Code/eos-sc/hello 280 | ``` 281 | 结果如下: 282 | 283 | ```cpp 284 | Reading WASM from /Users/zhangcheng/Code/eos-sc/hello/hello.wasm... 285 | Publishing contract... 286 | executed transaction: 9cb1a47cd1bfe749d0b4ab4f2e15c778f798aead1103962f4e230da8f1b83f19 1808 bytes 1701 us 287 | # eosio <= eosio::setcode {"account":"myuser","vmtype":0,"vmversion":0,"code":"0061736d01000000013b0c60027f7e006000017e60027e7... 288 | # eosio <= eosio::setabi {"account":"myuser","abi":"0e656f73696f3a3a6162692f312e30000102686900010475736572046e616d65010000000... 289 | ``` 290 | 291 | 接下来调用合约,通过`push action`命令来执行合约方法`hi`: 292 | 293 | ```cpp 294 | $ cleos push action myuser hi '["world"]' -p myuser 295 | ``` 296 | 结果如下: 297 | 298 | ```cpp 299 | executed transaction: a3781e50591348427eb61dd2d129d1a976977f67421da791f4a3cff6923d433c 104 bytes 588 us 300 | # myuser <= myuser::hi {"user":"world"} 301 | ``` 302 | 303 | 此时,`nodeos`的日志也发生了相应的变化,明确显示有一个交易。 304 | 305 | ```cpp 306 | info 2018-10-25T09:58:27.005 thread-0 producer_plugin.cpp:1490 produce_block ] Produced block 00003d2df2f11915... #15661 @ 2018-10-25T09:58:27.000 signed by eosio [trxs: 1, lib: 15660, confirmed: 0] 307 | ``` 308 | 309 | 至此,EOS的Hello World就结束了。 310 | 311 | 下一步,来一个高级点的应用,我们在EOS上发个币吧。 312 | -------------------------------------------------------------------------------- /kotlin-dsl.md: -------------------------------------------------------------------------------- 1 | # Kotlin构建自定义DSL 2 | 3 | >作者:丁筱颜 4 | 5 | ## DSL(Domain Specified Language)领域专用语言 6 | 7 | ### 常见的DSL 8 | 9 | - 正则表达式 10 | 11 | 通过一些规定好的符号和组合规则,通过正则表达式引擎来实现字符串的匹配 12 | 13 | - HTML&CSS 14 | 15 | 虽然写的是类似XML 或者 .{} 一样的字符规则,但是最终都会被浏览器内核转变成Dom树,从而渲染到Webview上 16 | 17 | - SQL 18 | 19 | 虽然是一些诸如 create select insert 这种单词后面跟上参数,这样的语句实现了对数据库的增删改查一系列程序工作 20 | 21 | ### DSL分类 22 | 23 | - 内部 DSL(从一种宿主语言构建而来) 24 | - 外部 DSL(从零开始构建的语言,需要实现语法分析器等) 25 | 26 | ## 通过Kotlin构建类型安全的DSL 27 | 28 | 例子: 29 | 30 | ```html 31 | html { 32 | head { 33 | title { +"XML encoding with Kotlin" } 34 | } 35 | body { 36 | h1 { +"XML encoding with Kotlin" } 37 | p { +"this format can be used as an alternative markup to XML" } 38 | 39 | // 一个具有属性和文本内容的元素 40 | a(href = "http://kotlinlang.org") { +"Kotlin" } 41 | 42 | // 混合的内容 43 | p { 44 | +"This is some" 45 | b { +"mixed" } 46 | +"text. For more see the" 47 | a(href = "http://kotlinlang.org") { +"Kotlin" } 48 | +"project" 49 | } 50 | p { +"some text" } 51 | 52 | // 以下代码生成的内容 53 | p { 54 | for (arg in args) 55 | +arg 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | 这是在kotlin中完全合法的一段代码,并且可以正确运行出结果,得到的结果如下图 62 | 63 | ```html 64 | 65 | 66 | 67 | XML encoding with Kotlin 68 | 69 | 70 | 71 |

72 | XML encoding with Kotlin 73 |

74 |

75 | this format can be used as an alternative markup to XML 76 |

77 | 78 | Kotlin 79 | 80 |

81 | This is some 82 | 83 | mixed 84 | 85 | text. For more see the 86 | 87 | Kotlin 88 | 89 | project 90 |

91 |

92 | some text 93 |

94 |

95 |

96 | 97 | 98 | 99 | ``` 100 | 101 | 这就是我们自定义DSL构造器得出的结果。 102 | 103 | 首先我们回顾一些kotlin技术: 104 | 105 | ### lambda与高阶函数 106 | 107 | Kotlin 的 lambda 有个规约:如果 lambda 表达式是函数的最后一个实参,则可以放在括号外面,并且可以省略括号,这个规约是 Kotlin DSL 实现嵌套结构的本质原因。 108 | 109 | 传递lambda表达式作为参数:`fun html(init: HTML.() -> Unit): HTML`,这个方法接收一个有receiver的lambda表达式,因为这样在block的内部就可以直接访问receiver的公共成员了,这一点也很重要。 110 | 111 | ### 扩展函数(扩展属性) 112 | 113 | 对于同样作为静态语言的 Kotlin 来说,扩展函数(扩展属性)是让他拥有类似于动态语言能力的法宝,即我们可以为任意对象动态的增加函数或属性。 114 | 115 | 比如,为 LocalDate 扩展一个函数:` toDate()`: 116 | 117 | ```kotlin 118 | fun LocalDate.toDate(): Date = Date.from(this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) 119 | 120 | ``` 121 | 122 | 与 JavaScript 这类动态语言不一样,Kotlin 实现原理是: 提供静态工具类,将接收对象(此例为 String )做为参数传递进来,以下为该扩展函数编译成 Java 的代码 123 | 124 | ```java 125 | @NotNull 126 | public static final Date toDate(@NotNull LocalDate $receiver) { 127 | Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); 128 | Date var10000 = Date.from($receiver.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); 129 | Intrinsics.checkExpressionValueIsNotNull(var10000, "Date.from(this.atStartOf…emDefault()).toInstant())"); 130 | return var10000; 131 | } 132 | 133 | //java call 134 | Date date = toDate(LocalDate.now()); 135 | //kotlin call 136 | val date = LocalDate.now().toDate() 137 | ``` 138 | 139 | 在Kotlin语言中,类不再是语言的最小单位。我们既可以单独声明一个全局函数,也可以声明全局变量。因此,你可以认为toLong是一个函数整体,这个函数的接收者可以是任意对象。 140 | 141 | 而对于Java,Java语言中所有的行为都必须在类体中完成。具体到某个函数或某一个变量始终属于某一个类实例。换而言之,其Receiver是固定的,也就没有了所谓Receiver的概念。 142 | 143 | ### 一元运算符 144 | 145 | ```kotlin 146 | operator fun BigDecimal.unaryPlus() = this.plus(java.math.BigDecimal.TEN) 147 | 148 | println(+BigDecimal("100")) 149 | //110 150 | ``` 151 | 152 | ## 开始分析 153 | 154 | ### 实现原理 155 | 156 | 首先看 157 | 158 | ``` 159 | html{ 160 | head{} 161 | body{} 162 | } 163 | ``` 164 | 165 | 这个代码块的实质是一个函数调用 166 | 167 | ```kotlin 168 | fun html(init: HTML.() -> Unit): HTML { 169 | val html = HTML() 170 | html.init() 171 | return html 172 | } 173 | ``` 174 | 175 | 这个函数接受一个名为 `init` 的参数,该参数本身就是一个函数。 该函数的类型是 `HTML.() -> Unit`,它是一个 *带接收者的函数类型* 。 这意味着我们需要向函数传递一个 HTML 类型的实例( *接收者* ), 并且我们可以在函数内部调用该实例的成员。 该接收者可以通过 *this* 关键字访问: 176 | 177 | ``` 178 | html { 179 | this.head { …… } 180 | this.body { …… } 181 | } 182 | ``` 183 | 184 | (`head` 和 `body` 是 `HTML` 的成员函数。) 185 | 186 | 现在,像往常一样,*this* 可以省略掉了,我们得到的东西看起来已经非常像一个构建器了: 187 | 188 | ``` 189 | html { 190 | head { …… } 191 | body { …… } 192 | } 193 | ``` 194 | 195 | 它创建了一个 `HTML` 的新实例,然后通过调用作为参数传入的函数来初始化它 (在我们的示例中,归结为在HTML实例上调用 `head` 和 `body`),然后返回此实例。 这正是构建器所应做的。 196 | 197 | `HTML` 类中的 `head` 和 `body` 函数的定义与 `html` 类似。 唯一的区别是,它们将构建的实例添加到包含 `HTML` 实例的 `children` 集合中: 198 | 199 | ```kotlin 200 | fun head(init: Head.() -> Unit) : Head { 201 | val head = Head() 202 | head.init() 203 | children.add(head) 204 | return head 205 | } 206 | 207 | fun body(init: Body.() -> Unit) : Body { 208 | val body = Body() 209 | body.init() 210 | children.add(body) 211 | return body 212 | } 213 | ``` 214 | 215 | 实际上这两个函数做同样的事情,所以我们可以有一个泛型版本,`initTag`: 216 | 217 | ```kotlin 218 | protected fun initTag(tag: T, init: T.() -> Unit): T { 219 | tag.init() 220 | children.add(tag) 221 | return tag 222 | } 223 | ``` 224 | 225 | 所以,现在我们的函数很简单: 226 | 227 | ```kotlin 228 | fun head(init: Head.() -> Unit) = initTag(Head(), init) 229 | 230 | fun body(init: Body.() -> Unit) = initTag(Body(), init) 231 | ``` 232 | 233 | 并且我们可以使用它们来构建 `` 和 `` 标签。 234 | 235 | 这里要讨论的另一件事是如何向标签体中添加文本。在上例中我们这样写到: 236 | 237 | ``` 238 | html { 239 | head { 240 | title {+"XML encoding with Kotlin"} 241 | } 242 | // …… 243 | } 244 | ``` 245 | 246 | 所以基本上,我们只是把一个字符串放进一个标签体内部,但在它前面有一个小的 `+`, 所以它是一个函数调用,调用一个前缀 `unaryPlus()` 操作。 该操作实际上是由一个扩展函数 `unaryPlus()` 定义的,该函数是 `TagWithText` 抽象类(`Title` 的父类)的成员: 247 | 248 | ```kotlin 249 | operator fun String.unaryPlus() { 250 | children.add(TextElement(this)) 251 | } 252 | ``` 253 | 254 | 所以,在这里前缀 `+` 所做的事情是把一个字符串包装到一个 `TextElement` 实例中,并将其添加到 `children`集合中, 以使其成为标签树的一个适当的部分。 255 | 256 | ### 作用域控制 257 | 258 | 由于内部的作用域默认可以获得外部的隐式接收器 259 | 260 | ``` 261 | html { 262 | head { 263 | head{ 264 | //无意义的head 265 | } 266 | } 267 | } 268 | ``` 269 | 270 | 我们可以使用`@DslMarker`来注释一个注解 271 | 272 | ```kotlin 273 | @Target(ANNOTATION_CLASS) 274 | @Retention(BINARY) 275 | @MustBeDocumented 276 | @SinceKotlin("1.1") 277 | public annotation class DslMarker 278 | 279 | @DslMarker 280 | annotation class HtmlTagMarker 281 | ``` 282 | 283 | 注释类`HtmlTagMarker`被称为*一个DSL标记*,它被注解`@DslMarker`注释。 284 | 285 | 一般规则: 286 | 287 | - 如果隐式接收*器用@HtmlTagMarker*相应的DSL标记注释标记,则它可以*属于* DSL 288 | - 同一DSL的两个隐式接收器在同一范围内不可访问 289 | - 就近原则 290 | - 其他可用的接收器可以照常解析,但如果得到的解析调用绑定到这样的接收器,则编译错误 291 | 292 | 标记规则:隐式接收器被视为被`@HtmlTagMarker`注释,需要满足下面的条件: 293 | 294 | - 它的类型是被标记,或 295 | - 它的类型分类器被标记 296 | - 或其任何超类/超接口 297 | 298 | 补充说明 299 | 300 | - `this@label`无论是否被标记,都可以访问接收器 301 | 302 | ### 完整代码 303 | 304 | ```kotlin 305 | package com.example.html 306 | 307 | interface Element { 308 | fun render(builder: StringBuilder, indent: String) 309 | } 310 | 311 | class TextElement(val text: String) : Element { 312 | override fun render(builder: StringBuilder, indent: String) { 313 | builder.append("$indent$text\n") 314 | } 315 | } 316 | 317 | @DslMarker 318 | annotation class HtmlTagMarker 319 | 320 | @HtmlTagMarker 321 | abstract class Tag(val name: String) : Element { 322 | val children = arrayListOf() 323 | val attributes = hashMapOf() 324 | 325 | protected fun initTag(tag: T, init: T.() -> Unit): T { 326 | tag.init() 327 | children.add(tag) 328 | return tag 329 | } 330 | 331 | override fun render(builder: StringBuilder, indent: String) { 332 | builder.append("$indent<$name${renderAttributes()}>\n") 333 | for (c in children) { 334 | c.render(builder, indent + " ") 335 | } 336 | builder.append("$indent\n") 337 | } 338 | 339 | private fun renderAttributes(): String { 340 | val builder = StringBuilder() 341 | for ((attr, value) in attributes) { 342 | builder.append(" $attr=\"$value\"") 343 | } 344 | return builder.toString() 345 | } 346 | 347 | override fun toString(): String { 348 | val builder = StringBuilder() 349 | render(builder, "") 350 | return builder.toString() 351 | } 352 | } 353 | 354 | abstract class TagWithText(name: String) : Tag(name) { 355 | operator fun String.unaryPlus() { 356 | children.add(TextElement(this)) 357 | } 358 | } 359 | 360 | class HTML : TagWithText("html") { 361 | fun head(init: Head.() -> Unit) = initTag(Head(), init) 362 | 363 | fun body(init: Body.() -> Unit) = initTag(Body(), init) 364 | } 365 | 366 | class Head : TagWithText("head") { 367 | fun title(init: Title.() -> Unit) = initTag(Title(), init) 368 | } 369 | 370 | class Title : TagWithText("title") 371 | 372 | abstract class BodyTag(name: String) : TagWithText(name) { 373 | fun b(init: B.() -> Unit) = initTag(B(), init) 374 | fun p(init: P.() -> Unit) = initTag(P(), init) 375 | fun h1(init: H1.() -> Unit) = initTag(H1(), init) 376 | fun a(href: String, init: A.() -> Unit) { 377 | val a = initTag(A(), init) 378 | a.href = href 379 | } 380 | } 381 | 382 | class Body : BodyTag("body") 383 | class B : BodyTag("b") 384 | class P : BodyTag("p") 385 | class H1 : BodyTag("h1") 386 | 387 | class A : BodyTag("a") { 388 | var href: String 389 | get() = attributes["href"]!! 390 | set(value) { 391 | attributes["href"] = value 392 | } 393 | } 394 | 395 | fun html(init: HTML.() -> Unit): HTML { 396 | val html = HTML() 397 | html.init() 398 | return html 399 | } 400 | ``` 401 | 402 | 403 | 404 | ## 应用 405 | 406 | - kotlin官方html构造器:[kotlinx.html](https://github.com/Kotlin/kotlinx.html) 407 | - kotlin的javaFX框架:[TornadoFX](https://github.com/edvin/tornadofx) 408 | 409 | - 安卓布局框架:[anko](https://github.com/Kotlin/anko) 410 | - kotlin服务端框架:[ktor](https://github.com/ktorio/ktor) 411 | 412 | ## 参考文档 413 | 414 | [类型安全的构建器](https://www.kotlincn.net/docs/reference/type-safe-builders.html) 415 | 416 | [Scope control for implicit receivers](https://github.com/Kotlin/KEEP/blob/master/proposals/scope-control-for-implicit-receivers.md) 417 | 418 | [Kotlin之美——DSL篇](https://www.jianshu.com/p/f5f0d38e3e44) 419 | -------------------------------------------------------------------------------- /kotlin-tips-and-tricks.md: -------------------------------------------------------------------------------- 1 | # Kotlin tips and tricks 2 | 3 | 一篇小文,说说kotlin带来方便的一些特性。 4 | 5 | #### IntelliJ IDEA对Kotlin的支持 6 | 同一个团队的产品,所以在kotlin一出生就有完美的IDE支持。对于Java程序员要学习Kotlin,开始部分语法会遇到难题,用IDE有两个小trick: 7 | 8 | - 直接复制Java代码到Kotlin文件中,可以直接转变为Kotlin代码,然后学习。 9 | - 也可以通过菜单中的工具,将整个Java类转变为Kotlin代码。 10 | 11 | 但,这样的功能用得越少,学习Kotlin也就越快。 12 | 13 | #### var和val 14 | 老生长谈了,var是可变的,val不可变,并发安全。建议val变量在定义之初就要初始化。 15 | 16 | #### 表达式函数体 17 | 定义一个函数,可以有更简单的形式,注意是函数体只有一个语句的情况。 18 | 19 | ```java 20 | // 一般形式 21 | fun max(a: Int, b: Int): Int { 22 | return if (a > b) a else b 23 | } 24 | // 表达式函数体 25 | fun max(a: Int, b: Int): Int = if (a > b) a else b 26 | ``` 27 | 对于上面的特性,NTELLIJ IDEA也有提示,可以通过工具转换。 28 | 29 | #### 类型推导 30 | Kotlin强类型的语言,但有些场景类型可以省略,由编译器进行推断。 31 | 32 | ```java 33 | // 类型推断 34 | val a = SomeClass() 35 | // 上面的函数可以重新定义 36 | fun max(a: Int, b: Int) = if (a > b) a else b 37 | ``` 38 | 39 | #### 类型智能转换 40 | 最喜欢的功能之一。还记得Java里类型转换的括号嘛,我们看看Kotlin的做法 41 | 42 | ```java 43 | // obj不需要像Java一样强制转换,!is也是一样 44 | if (obj is String) { 45 | print(obj.length) 46 | } 47 | // 当然,这个也可以 48 | if (obj !is String || obj.length == 0) return 49 | // 还有强大的when 50 | when (x) { 51 | is Int -> print(x + 1) 52 | is String -> print(x.length + 1) 53 | is IntArray -> print(x.sum()) 54 | } 55 | ``` 56 | 除此之外,Kotlin也支持强制类型的转换,as和as 57 | 58 | ```java 59 | // 非安全转换,会抛出异常,如果y为null 60 | val x: String = y as String 61 | // 安全转换,如果y为null,直接返回null 62 | val x: String = y as? String 63 | ``` 64 | 65 | #### 快速的创建数组 66 | 对比Java,会有一些方便 67 | 68 | ```java 69 | val a: Array = arrayOf(1,2,3) 70 | val b:Array = Array(3,{k -> k*k}) 71 | ``` 72 | 73 | #### import重命名 74 | 算是一个特性,我们实践中很少能够用到了。 75 | 76 | ```java 77 | // 神奇,会优先使用import进来的 78 | import Base as Hello 79 | 80 | class Hello { 81 | fun hello() { 82 | println("hello world") 83 | } 84 | } 85 | ``` 86 | 87 | #### range区间 88 | 方便的语法糖,记住类型就好 89 | 90 | ```java 91 | // 闭区间 92 | val range:IntRange = 1 .. 5 93 | // 左闭右开 94 | val range : IntRange = 1 until 5 95 | ``` 96 | 97 | #### 控制流表达式 98 | 与Java不同,if .. elas / try ..catch在Kotlin都是表达式,可以出现在等号的右边。 99 | 100 | ```java 101 | val max = if (a > b) a else b 102 | // 还有一个相关的小特性,顺便说说 103 | fun testIfReturn(a: Int, b: Int) { 104 | 105 | val max = if (a > b) { 106 | print("Max a") 107 | a // 代码块最后一行表达式的值就是 max 的值 108 | } else { 109 | print("Max b") 110 | b 111 | } 112 | // 字符串插值,以前写Java羡慕了很久Perl 113 | println("max = $max") 114 | } 115 | ``` 116 | 117 | #### 代理(by) 118 | 代理模式提供一种实现集成的替代方法,Kotlin原生就支持。 119 | 120 | ```java 121 | // 代理类 122 | interface Base { 123 | fun print() 124 | } 125 | class BaseImpl(val x: Int) : Base { 126 | override fun print() { print(x) } 127 | } 128 | 129 | class Derived(b: Base) : Base by b 130 | 131 | fun main(args: Array) { 132 | val b = BaseImpl(10) 133 | Derived(b).print() // prints 10 134 | } 135 | // 代理属性 136 | // 1. 延迟属性:第一次真正访问的时候,初始化 137 | // 2. 观察属性:属性发生改变,通知监听者 138 | // 3. Map中存储属性,有趣 139 | class Example { 140 | var p: String by Delegate() 141 | } 142 | class Delegate { 143 | operator fun getValue(thisRef: Any?, property: KProperty<*>): String { 144 | return "$thisRef, delegating '${property.name}' to me!" 145 | } 146 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { 147 | println("$value assigned to '${property.name} in $thisRef.'") 148 | } 149 | } 150 | // 读取p值 151 | val e = Example() 152 | println(e.p) //Example@33a17727, delegating ‘p’ to me! 153 | // 设置p值 154 | e.p = "NEW" 155 | // 打印结果: 156 | // NEW assigned to ‘p’ in Example@33a17727. 157 | // Lazy简单用法 158 | val lazyValue: String by lazy { 159 | println("computed!") 160 | "Hello" 161 | } 162 | 163 | fun main(args: Array) { 164 | println(lazyValue) 165 | println(lazyValue) 166 | } 167 | // prints: 168 | // computed! 169 | // Hello 170 | // Hello 171 | 172 | // 可观察方法简单用法,这个在业务系统中,可以有不错的尝试 173 | class User { 174 | var name: String by Delegates.observable("") { 175 | prop, old, new -> 176 | println("$old -> $new") 177 | } 178 | } 179 | fun main(args: Array) { 180 | val user = User() 181 | user.name = "first" 182 | user.name = "second" 183 | } 184 | // 结果: 185 | // < nomalName > -> first 186 | // first -> second 187 | ``` 188 | 189 | #### sealed class 190 | 密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。 191 | 192 | ```java 193 | // Kotlin 1.1+,否则子类必须在sealed class内部 194 | sealed class Expr 195 | data class Const(val number: Double) : Expr() 196 | data class Sum(val e1: Expr, val e2: Expr) : Expr() 197 | object NotANumber : Expr() 198 | 199 | // 作用1 保护代码,感受不深 200 | // 作用2 when可以没有else了 201 | fun eval(expr: Expr): Double = when(expr) { 202 | is Const -> expr.number 203 | is Sum -> eval(expr.e1) + eval(expr.e2) 204 | NotANumber -> Double.NaN 205 | // 不再需要 `else` 子句,因为我们已经覆盖了所有的情况 206 | } 207 | ``` 208 | 209 | #### 高阶函数 210 | 高阶函数是将函数用作参数或返回值的函数。 211 | 212 | ```java 213 | fun Collection.fold( 214 | initial: R, 215 | combine: (acc: R, nextElement: T) -> R 216 | ): R { 217 | var accumulator: R = initial 218 | for (element: T in this) { 219 | accumulator = combine(accumulator, element) 220 | } 221 | return accumulator 222 | } 223 | // 具体使用示例 224 | val items = listOf(1, 2, 3, 4, 5) 225 | 226 | // Lambdas 表达式是花括号括起来的代码块。 227 | items.fold(0, { 228 | // 如果一个 lambda 表达式有参数,前面是参数,后跟“->” 229 | acc: Int, i: Int -> 230 | print("acc = $acc, i = $i, ") 231 | val result = acc + i 232 | println("result = $result") 233 | // lambda 表达式中的最后一个表达式是返回值: 234 | result 235 | }) 236 | 237 | // lambda 表达式的参数类型是可选的,如果能够推断出来的话: 238 | val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i }) 239 | 240 | // 函数引用也可以用于高阶函数调用: 241 | val product = items.fold(1, Int::times) 242 | ``` 243 | 244 | #### 带有接收者的函数字面值 245 | 带有接受者的函数类型,就是这样: 246 | 247 | ```java 248 | A.(B) -> C 249 | ``` 250 | 我第一次看到是懵的,但给一个例子,就好多了。 251 | 252 | ```java 253 | val sum = fun Int.(other: Int): Int = this + other 254 | println(sum(5 + 6)) // 11 255 | ``` 256 | 是不是还没有透彻,来一个完整的,很强大,可以尽情玩耍。 257 | 258 | ```java 259 | class HTML { 260 | fun body() { …… } 261 | } 262 | 263 | fun html(init: HTML.() -> Unit): HTML { 264 | val html = HTML() // 创建接收者对象 265 | html.init() // 将该接收者对象传给该 lambda 266 | return html 267 | } 268 | 269 | html { // 带接收者的 lambda 由此开始 270 | body() // 调用该接收者对象的一个方法 271 | } 272 | ``` 273 | 为什么可以这样呢? 274 | 275 | ```java 276 | html { 277 | // …… 278 | } 279 | ``` 280 | html是一个函数调用,它接受一个lambda表达式为参数,如下: 281 | 282 | ```java 283 | fun html(init: HTML.() -> Unit): HTML { 284 | val html = HTML() 285 | html.init() 286 | return html 287 | } 288 | ``` 289 | 这个函数接受一个名为 init 的参数,该参数本身就是一个函数。 该函数的类型是 HTML.() -> Unit,它是一个 带接收者的函数类型 。 这意味着我们需要向函数传递一个 HTML 类型的实例( 接收者 ), 并且我们可以在函数内部调用该实例的成员。 该接收者可以通过 this 关键字访问: 290 | 291 | ```java 292 | html { 293 | this.head { …… } 294 | this.body { …… } 295 | } 296 | ``` 297 | 省略this 298 | 299 | ```java 300 | html { 301 | head { …… } 302 | body { …… } 303 | } 304 | ``` 305 | 完整示例可见于Kotlin官方文档。 306 | 307 | #### 函数扩展 308 | 之前使用Go开发的时候,着实羡慕了一把,不过现在Kotlin也可以了,话不多说 309 | 310 | ```java 311 | fun MutableList.swap(index1: Int, index2: Int) { 312 | val tmp = this[index1] // “this”对应该列表 313 | this[index1] = this[index2] 314 | this[index2] = tmp 315 | } 316 | ``` 317 | 这个功能这个在平时使用得也是比较多的。其本质不是真正的修改所扩展的类,而是静态的解析的,类似静态的方法,那以下的case就要注意了。 318 | 319 | ```java 320 | open class C 321 | 322 | class D: C() 323 | 324 | fun C.foo() = "c" 325 | 326 | fun D.foo() = "d" 327 | 328 | fun printFoo(c: C) { 329 | println(c.foo()) 330 | } 331 | printFoo(D()) // 这里是c 332 | ``` 333 | 再来一个知乎的例子,更全面 334 | 335 | ```java 336 | operator inline fun Int.rem(blk: () -> Unit) { 337 | if (Random (System.currentTimeMillis()).nextInt(100) < this) blk() 338 | } 339 | // 开始调用 340 | 20 % { print ("你有20%的概率看到这条信息") } 341 | ``` 342 | 343 | #### 中缀表示法 344 | 345 | infix关键字,标识中缀函数: 346 | 347 | - 它们必须是成员函数或扩展函数; 348 | - 它们必须只有一个参数; 349 | - 其参数不得接受可变数量的参数且不能有默认值。 350 | 351 | ```java 352 | infix fun Int.shl(x: Int): Int { …… } 353 | 354 | // 用中缀表示法调用该函数 355 | 1 shl 2 356 | 357 | // 等同于这样 358 | 1.shl(2) 359 | ``` 360 | 361 | #### 可变数量的参数(Varargs) 362 | 函数的参数(通常是最后一个)可以用 vararg 修饰符标记,这里也可以用伸展(spread)操作符(在数组前面加 *): 363 | 364 | ```java 365 | fun asList(vararg ts: T): List { 366 | val result = ArrayList() 367 | for (t in ts) // ts is an Array 368 | result.add(t) 369 | return result 370 | } 371 | // 如果要传一个数组 372 | val a = arrayOf(1, 2, 3) 373 | val list = asList(-1, 0, *a, 4) 374 | ``` 375 | 376 | #### use函数 377 | 实现了Closeable接口的对象可调用use函数,自动close。不举例了。 378 | 379 | #### with、let、also、run and apply 380 | 用一张图和一段代码说明: 381 | 382 | ![](1_pLNnrvgvmG6Mdi0Yw3mdPQ.png) 383 | 384 | 代码如下: 385 | 386 | ```java 387 | // 代码转载自简书 388 | class Resp { 389 | var code: Int = 0 390 | var body: T? = null 391 | var errorMessage: String? = null 392 | 393 | fun isSuccess(): Boolean = code == 200 394 | 395 | override fun toString(): String { 396 | return "Resp(code=$code, body=$body, errorMessage=$errorMessage)" 397 | } 398 | } 399 | fun main(args: Array) { 400 | var resp: Resp? = Resp() 401 | if (resp != null) { 402 | if (resp.isSuccess()) { 403 | // do success 404 | println(resp.body) 405 | } else { 406 | println(resp.errorMessage) 407 | } 408 | } 409 | 410 | resp?.run { 411 | if (isSuccess()) { 412 | // do success 413 | println(resp.body) 414 | } else { 415 | println(resp.errorMessage) 416 | } 417 | } 418 | 419 | resp?.apply { 420 | if (isSuccess()) { 421 | // do success 422 | println(resp.body) 423 | } else { 424 | println(resp.errorMessage) 425 | } 426 | } 427 | 428 | resp?.let { 429 | if (it.isSuccess()) { 430 | // do success 431 | println(it.body) 432 | } else { 433 | println(it.errorMessage) 434 | } 435 | } 436 | 437 | resp?.also { 438 | if (it.isSuccess()) { 439 | // do success 440 | println(it.body) 441 | } else { 442 | println(it.errorMessage) 443 | } 444 | } 445 | } 446 | ``` 447 | 448 | #### typealias 449 | 450 | ```java 451 | typealias Cache = HasmMap 452 | ``` 453 | 454 | 455 | #### Java调用Kotlin属性 456 | 457 | ```java 458 | // kotlin 459 | var a: Int = 1 460 | 461 | // java 462 | someone.setA(2) 463 | someone.getA() // 2 464 | ``` 465 | 466 | #### Java调用Kotlin方法 467 | 468 | ```java 469 | // Counter.kt 470 | fun count() = 42 471 | 472 | // Java class 473 | int size = CounterKt.count() 474 | ``` 475 | CounterKt?不优雅 476 | 477 | ```java 478 | // Counter.kt 479 | @file:JvmName("Counter") 480 | fun count() = 42 481 | 482 | // Java class 483 | int size = Counter.count() 484 | ``` 485 | 486 | #### Java调用Kotlin静态方法 487 | 488 | ```java 489 | // Kotlin 490 | companion object { 491 | fun count() = 42 492 | } 493 | 494 | // Java class 495 | int size = Counter.Companion.count() 496 | ``` 497 | 不够优雅~ 498 | 499 | ```java 500 | // Kotlin 501 | companion object { 502 | @JvmStatic 503 | fun count() = 42 504 | } 505 | 506 | // Java class 507 | int size = Counter.count() 508 | ``` 509 | #### Java调用Kotlin构造函数 510 | 511 | ```java 512 | // Kotlin 513 | class Person(val fullName: String, val nickName: String? = null) 514 | // Java 515 | Person person = new Person("Lorenzo"); // error 516 | 517 | // Kotlin 518 | class Person @JvmOverloads constructor ( 519 | val fullName: String, 520 | val nickName: String? = null 521 | ) 522 | // Java 523 | Person person = new Person("Lorenzo"); // ok 524 | ``` 525 | 526 | --------------------------------------------------------------------------------