typeof Diary

VimとかJSとか。やったことのメモ。自分のため。

iTerm2からAlacritty+tmuxにしてみた

今までGVimだったんですけど、周り見てみると、普通にターミナル環境でVim触ってる人多いですよね。

Vimにターミナルが入ったとはいえ、あまり使いこなせていないのも現状。
あと、VSCodeではこの感じでずっとやってたので、こんな感じにしたいなーと思った次第。
f:id:lisia:20191117105018p:plain

+なんかツイッターで「Alacritty」なるものを見かけたので、ちょっと飛びついてみました。

Alacrittyを導入

AlacrittyRust製の高速なターミナルエミュレータらしい。

公式に書いてあるとおり、brea cask install alacrittyで入れます。

設定

$XDG_CONFIG_HOME/alacritty/alacritty.yml
$XDG_CONFIG_HOME/alacritty.yml
$HOME/.config/alacritty/alacritty.yml
$HOME/.alacritty.yml

ここのどこかに。
自分は.config/alacritty/alacritty.ymlにできてました。
上から順に辿るとのこと。

見て分かるように、設定はyamlファイルです。
少しだけ設定変えました。

window:
  # デフォ0,0でやたら画面端から描画されるので、余白ちょっととる
  padding:
    x: 5
    y: 5
font:
  normal:
    family: "Ricty for Powerline"
  size: 12.5

フォントサイズ小さい言われるけど、ある程度小さめで、全体見渡したいんです。
(でもミニマップは不要派)

Ricty for Powerlineも長年お世話になってます。

tmuxを導入

次にtmux。
便利なのは聞いてたんですけど、とくに不自由もしてなかったので、導入してませんでした。
が、しかし。

「タブ増やしたり、ペイン分割したい」と思っても、Alacrittyだけではできなさそう。

Why isn't feature X implemented?

which are best left to a window manager or terminal multiplexer

そういうのはウィンドウマネージャとかターミナルマルチプレクサーがええよ!みたいな。
だったら、重い腰上げて導入しようじゃないか。が経緯です。

brew install tmux

設定

~/.tmux.conf

# デフォルトのC-bが打ちにくい。
set -g prefix C-a

# なんかいろいろ ...
set -g escape-time 0

bind | split-window -h
bind - split-window -v
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

見かけた設定を入れてみつつ、ちゃっかりペイン移動をhjklにしておく感じです。

やった感想

tmux良いーー。なんで導入してこなかったんだ・・・。
Alacrittyが速いっていうのは、正直なところあんまり体感できてないのですが、、、
(そこまでiTermとかで気にしたことがなかったから)

こういう機会を作ってくれたことには感謝です。

今後体感できるときがくるかもしれないです。。 f:id:lisia:20191117105059p:plain

VSCodeからVimに戻った

去年の末ぐらいからVSCodeに浮気をして、VSCodeを使っていました。
とはいえ、VSCodeVim拡張を入れて使っていたわけですが。。。

昨年末、Angularを書く機会があって、Vimでは書きにくかったのもあって移行したのでした。
結構不自由なく使えていて、不満もなくしばらくは使っていました。

ところが・・・。

VSCodeVIM is unusable (incredibly slow) on larger files, especially in INSERT MODE

この類のIssueにもあるように、とある部分でパフォーマンスが著しく落ちるのです。
体感1000行あるときっついなーとは思ってます。
新規案件で最初から作っていくならともかく、既存のもを改修するタイプでは厳しかったです。
(最初は、これが遅くなるようならもっと減らせるんじゃない?みたいな指標にしてたりもしましたが)

せっかくだから、貢献できないのかな?とも考えたのですが・・・。
力及ばず。

結局Angularの案件もポシャってしまって、惰性でそのままVSCodeを使っていましたが、段々としんどくなってくるわけです。
遅いなーと思うファイルもVimで開けば快適。

PHPStormなんかもいいなーと社内のPHPStorm勢を横目に、
いい機会だしvimrc整理から始めるかー!と、重い腰を上げた次第です。

少し身を引いていた間の環境の変化に驚く

マッピング周りはほぼ変えず、無駄に入れては使っていないプラグインや、乗り換えができるなら乗り換え先を探して、そこの整理を主にやりました。

プラグインマネージャはdeinからvim-plugに乗り換え。分かりやすく、シンプルでした。
ファイラはvim-filer+依存していたuniteからVaffleに。

さて、補完周り。

VimConfを探ってみると、何やらとcoc.nvimなるものがあるらしい。

ここがネオコンからcoc.nvimになりました。

その他、入れたはいいけど使ってないプラグインなどを削除していった結果、60ちょいあったのが、30ぐらいまで減りました。
なんやかんやよく分からず行数だけ増えてた.vimrcも700ぐらいから370ぐらい?まで減りました。

coc.nvimへの驚き

似たような記事にいろいろ出くわしたのですが、やっぱりすごかったcoc.nvim。
元々TS書きにくいなー、tsuquyomiとかあるにはあるけど、VSCode楽だなーみたいな理由で乗り換えていたので、こいつのおかげでTSも普通に書けるじゃないですか!?

当初の移行した理由がカイゼンされるのなら、Vimでいいような・・・。

定義ジャンプもできるんですか!?
あっ・・・。
元鞘・・・。

f:id:lisia:20191116164315g:plain

またよろしくVim

おわり。

CSSでDOMを非表示にするパターン比較的なやつ

JavaScript書いていると、「DOMを非表示にしておいて、特定条件で表示する」みたいなのってよく書きます。
一個一個は単純でも、複数になるとそこそこ面倒。。。

今回は、この非表示にする方法にフォーカスしてみるやつです。
なんでもかんでもdisplay: none;使ってない!?ちょっと考えて使おうね!が趣旨。

CSSで非表示する方法

以下の3つ。

  • display: none;
  • visibility: hidden;
  • opacity: 0;

display:noneはおなじみですね。
他は?opacityはなんとなく使ったことあるけど、visibilityってあまり馴染みないような。

だけど、意外と使い分けが大事な場面もあったり。
それぞれ違いを見ていきます。

違いは?

単純な違い

まずdisplay:none
display:noneのデモ 触ってもらえば分かると思いますが、DOMが消えて、「高さ」なども消滅しています。

次にvisibility: hidden
visibility: hiddenのデモ
display:noneと違って、表示は消えても「高さ」などは残っていることが分かります。

最後にopacity:0
opacity: 0のデモ
これも表示は消えて、「高さ」などは残ることが分かります。

ここまでで分かるのは、がっつり消してしまいたいならdisplay: none
高さとか残しときたいならvisibility: hiddenopacity: 0ってな具合でしょう。

これだけなら好みでどうぞ!って感じなんですが、そんなわけないです。

イベント発火

イベント発火の例

次にそれぞれにイベントを付与してみます。
toggleを押す前、押した後で、clickしてみると違いが浮き彫りに。

display: noneはそもそも何もないので、イベントはおきない。
visibility: hiddenも同じくイベントはおきない。
opacity: 0だけはイベントが発生。

opacityは透過度を指定しているだけなので、実態はそこにある状態。
なのでイベントも効いてきますね(考えてみたら当然)。

対象にイベントが付与されている場合は、display:noneか、visibility:hidden
一昔前の隠しリンク的なのがやりたいならopacity:0ですね。

DOMの取得とか

サイズとかポジションとる例

まずGetSizeして、toggleしてGetSizeしてみると分かると思いますが。
display: noneだとサイズもポジションも取れないので、DOMのポジション使って何かするような処理書いてる場合は注意です。

例えば、対象要素の真下にドロップダウンリスト出すーとか、そんなパターンのやつ。

意外と

サンプル書いて、こうやって並べてみると当たり前に思えるけど、実際ハマることがあるので、
何が適切か考えながら使おうねって話でした。

JavaScriptのProxyって結構便利なのでは

Proxyオブジェクトが結構便利だったので書いてみます。

Proxy

Proxy オブジェクトは、基本的な操作 (例えばプロパティの検索、代入、列挙、関数の起動など) について独自の動作を定義するために使用します。

const proxy = new Proxy(targetObject, handler)

targetObjectに対象のオブジェクト、handlerにはそのオブジェクトに対してどうするのか、トラップと呼ばれるメソッドを定義して渡します。
よく使うであろうgetsetだけ紹介しときます。

get

const user = {
  name: 'Alice',
  age: 30
}

const proxy = new Proxy(user, {
  get(target, name) {
    if (target[name] <= 30) return 17
    return target[name]
  }
})

console.log(proxy.age) // 17

getconsole.logなど、そのプロパティにアクセスがあったとき、どうやって返すのかを定義できます。
例の場合はageが30以上なら17と表示されます。永遠のなんとやら・・・。

set

次にset。

const user = {
   name: 'Alice',
   age: null
}

const proxy = new Proxy(user, {
  set(target, name, value) {
    if (name === 'age') {
      target[name] = 17
      return true
    }
    return Reflect.set(...arguments)
  }
})

proxy.age = 30
console.log(proxy.age) // 17

例は何をいれても17になります(永遠の...)

使い所

表なりなんなり、データを一覧で表示しないといけないときとか。
条件として、nullとか空文字は-で表示してー!みたいな場合。

普通に書くと。。。

const rows = dataList.map(data => {
  return `
    <tr>
      <td>${data.A !== null ? data.A : '-'}</td>
      <td>${data.B !== null ? data.B : '-'}</td>
      <td>${data.C !== null ? data.C : '-'}</td>
      <td>${data.D !== null ? data.D : '-'}</td>
    </tr>
  `
})

// tbodyなりにrowsをappend

何も意識せず書くとこんな感じでテーブルのデータ組むと思います。
まとめるならこんな感じ。

const formatter = value => value !== null ? value : '-'
const rows = dataList.map(data => {
  return `
    <tr>
      <td>${formatter(data.A)}</td>
      <td>${formatter(data.B)}</td>
      <td>${formatter(data.C)}</td>
      <td>${formatter(data.D)}</td>
    </tr>
  `
})

やってることが単純なので、このレベルならこれで十分ですね正直。。。

Proxyを使うと、

const rows = dataList.map(data => {
  const p = new Proxy(data, {
    get(target, name) {
      if (target[name] === null) return '-'
      return target[name]
    }
  })
  return `
    <tr>
      <td>${p.A}</td>
      <td>${p.B}</td>
      <td>${p.C}</td>
      <td>${p.D}</td>
    </tr>
  `
})

HTML作ってる部分はスッキリしますね。

その他

ループでnewしまくるのが良いかどうのかってのはちょっとありそうですが、便利な機能なのでもう少し突き詰めたいところ 。

setなんかはオブジェクトの変更監視もできる気がするので、いわゆるオレオレstore的なのが作れるかも?
Vue3.0でProxyが使われてるとかそんな話を聞いたことがあるようなないような。

そもそもProxyってこんな使い方して良いのかなという疑問は残るんですが。
どうなんでしょう?

今更WebComponentsの触り

わりとガンガンjQueryな、とくにUIライブラリも使っていない。(jQueryUIはあるけど)
いろいろと辛い状況なやつ。

画面追加がある度に、ほぼその画面に合わせてCSSも書きつつ。
レイアウト指示はそこそこ細かく、px単位でたまに指摘される。

そんなのが続くと画面追加があるたびに、HTML、CSS書くのめんどくさい!!!という感情が湧いてくるわけです。
WebComponentsって楽できないのかな・・・がはじまり。

書き方基礎

<hello-world/>
class HelloWorld extends HTMLElement {
  constructor() {
    super()
    const shadowRoot = this.attachShadow({ mode: 'open" })
    shadowRoot.innerHTML = `<h1>Hello World</h1>`
  }
}

customElements.define('hello-world', HelloWorld)

書き方は意外と簡単ですね。

プロパティ与えたい

React, Vue, Angularなんかを書いていると、コンポーネントにpropsを渡して描画することが多々あります。
似たようなことできないの?って思ったんですが、厳しそうです。

<hello-world data-name="Bob"/>
const HelloWorld extends HTMLElement {
  constructor() {
    super()
    
    this.myName = this.getAttribute('data-name')
      ? this.getAttribute('data-name')
      : ''

    const shadowRoot = this.attachElement({ mode: 'open' })
    shadowRoot.innerHTML = this.template()
  }

  template() {
    return `<h1>Hello ${this.myName}`
  }
}

customElements.define('hello-world', HelloWorld)

Attributeでプロパティは渡せるけど、取り出したらこれ文字列ですよね。
無理矢理JSONなんか渡したらいけるのかもしれないけど、そこまでするか・・・?

ちょっとしたステータスで色変えたいよ。とか、そんなレベルで使うのが良いのかなと思いました。

いわゆるJS FW系で扱ってるようなコンポーネントとは似て非なるもの。って感じでしょうか。

まだ詳しくは調べてないけど、間違ってたらすいません。

A Tour of Goを完走した

1周目はよくわからなかったけど、よく読んで2周目やってみたら意外と解けました。

ということで、一通りやってみたコードのまとめ。
もっとスマートにできたりすると思う。

Loops and Functions

関数とループを使った簡単な練習として、平方根の計算を実装してみましょう: 数値 x が与えられたときに z² が最も x に近い数値 z を求めたいと思います。

func Sqrt(x float64) float64 {
    z := 1.0
    for i := 0; i < 10; i++ {
        z -= (z*z - z) / (2 * z)
    }
    return z
}

次に値が変化しなくなった (もしくはごくわずかな変化しかしなくなった) 場合にループを停止させます。 それが 10 回よりも多いか少ないかを確認してください。

この文章が本当に理解できず、悩みました。
「ごくわずかな変化ってなんやねん」って感じで・・・。

下の方にニュートン法のことが触れられていて、そっちを読むとなんとなく理解し始めました。

func Sqrt(x float64) float64 {
    z := 1.0
    var prev float64
    for i := 0; i < 10; i++ {
        z -= (z*z - z) / (2 * z)

        if math.Abs(prev-z) < 1e-10 {
            fmt.Printf("%d回目\n" , i+1)
            break
        }

        prev = z
    }
    return z
}

どんなもんなのかを課題解いたよ!って記事のコードいくつか読んでみて噛み砕いてからやりました。
ただ、1e-10のところが実装者によって様々で、1e-101e-60.0001とかいろいろあって、理解していないときは「なんで?」ってなりました。
ここが誤差の許容範囲みたいな感じで理解してます。

Slices

Pic 関数を実装してみましょう。 このプログラムを実行すると、生成した画像が下に表示されるはずです。

ここは問題文通り、+ヒント参考にしながらやればなんとかなりました。

func Pic(dx, dy, int) [][]uint8 {
    pic := make([][]uint8, dy)

    for y, _ := range pic {
        pic[y] = make([]uint8, dx)

        for x, _ := range pic[y] {
            pic[x][y] = uint8(x*y)
        }
    }

    return pic
}

uint8(xxx)のところは、

生成する画像は、好きに選んでください。例えば、面白い関数に、 (x+y)/2 、 x*y 、 xy などがあります。

の部分なので、好きな計算式入れてあげると良いです。
画像変わって面白いですよ。なんならここに示されている計算式以外でも変な画像できます。

Maps

WordCount 関数を実装してみましょう。string s で渡される文章の、各単語の出現回数のmapを返す必要があります。 wc.Test 関数は、引数に渡した関数に対しテストスイートを実行し、成功か失敗かを結果に表示します。

一番分かりやすかった気がするのは、普段JSで大体なんでもオブジェクトでKey:Valueにしてなんかしちゃうのが原因?

func WordCount(s string) map[string]int {
    wordList := strings.Fields(s)
 
    m := make(map[string]int)
    for _, word := range wordList {
        if _, exist := m[word]; exist {
            m[word]++
        } else {
            m[word] = 1
        }
    }
 
    return m
}

Fibonacci closure

fibonacci (フィボナッチ)関数を実装しましょう。この関数は、連続するフィボナッチ数(0, 1, 1, 2, 3, 5, ...)を返す関数(クロージャ)を返します。

クロージャの問題。
フィボナッチ数列自体は分かってたのですが、最初の0,1は固定なんですね・・・。

func fibonacci() func() int {
    fib := 0
    result := [2]int{0,0}

    return func() int {
        if fib == 1 {
            result[1] = 1
        } else {
            w := result[1]
            result[1] = result[0] + result[1]
            result[0] = w
        }
        fib++
        return result[1]
    }
}

さすがにもっとスマートに書けるんじゃないかなこれ。

Stringers

IPAddr 型を実装してみましょう IPアドレスをドットで4つに区切った( dotted quad )表現で出力するため、 fmt.Stringer インタフェースを実装してください。 例えば、 IPAddr{1, 2, 3, 4} は、 "1.2.3.4" として出力するようにします。

func (ip IPAddr) String() string {
    return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

Errors

Sqrt 関数を 以前の演習 からコピーし、 error の値を返すように修正してみてください。 Sqrt は、複素数をサポートしていないので、負の値が与えられたとき、nil以外のエラー値を返す必要があります。

type ErrNegativeSqrt float64

func (x ErrNegativeSqrt) Error() string {
    return fmt.Sprintf("cannot Sqrt negative number %f", float64(x))
}

func Sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, ErrNegativeSqrt(x)
    }

    z := 1.0
    var prev float64
    for i := 0; i < 10; i++ {
        z -= (z*z - z) / (2 * z)

        if math.Abs(prev-z) < 1e-10 {
            fmt.Printf("%d回目\n" , i+1)
            break
        }

        prev = z
    }
    return z, nil
}

Readers

ASCII文字 'A' の無限ストリームを出力する Reader 型を実装してください。

無限ストリームがイマイチピンとこなくて調べました。 ほぼそのままの意味でした()

b[i] = "A"としていてtype errorになって、ここではじめて'A'であることに気付いた。
学び。

otiai10.hatenablog.com

func (r MyReader) Read(b []byte) (int, error) {
    for i := range b {
        b[i] = 'A'
    }
    return len(b), nil
}

rot13Reader

io.Reader を実装し、 io.Reader でROT13 換字式暗号( substitution cipher )をすべてのアルファベットの文字に適用して読み出すように rot13Reader を実装してみてください。

type rot13Reader struct {
    r io.Reader
}

func (r rot13Reader) Read(b []byte) (n int, err error) {
    n, err = r.r.Read(b)
    if err != nil {
        return
    }

    for i, c := range b[:n] {
        if 'A' <= c && c <= 'Z' {
            b[i] = (c-'A'+13)%26 + 'A'
        } else if 'a' <= c && c <= 'z' {
            b[i] = (c-'a'+13)%26 + 'a'
        }
    }
    return
}

問題文中にあるgzip.NewReaderへのリンクがあったので、コード読んでると、r.r.Readのヒントが得られました。
読んだら書けました。

Image

前に解いた、画像ジェネレーターを覚えていますか? 今回は、データのスライスの代わりに image.Image インタフェースの実装を返すようにしてみましょう。 自分の Image 型を定義し、 インタフェースを満たすのに必要なメソッド を実装し、 pic.ShowImage を呼び出してみてください。

ドキュメント読んだらそれっぽくできました。
とりあえず、w, hだけ持たせて、呼び出しで大きさ決めれるようにはしておきました。

import (
    "golang.org/x/tour/pic"
    "image"
    "image/color"
)

type Image struct {
    w int
    h int
}

func (i Image) ColorModel() color.Model {
    return color.RGBAModel
}
func (i Image) Bounds() image.Rectangle {
    return image.Rect(0, 0, i.w, i.h)
}
func (i Image) At(x int, y int) color.Color {
    return color.RGBA{0, 0, uint8(x ^ y), uint8(x ^ y)}
}

func main() {
    m := Image{180, 180}
    pic.ShowImage(m)
}

Exercise: Equivalent Binary Trees

https://go-tour-jp.appspot.com/concurrency/7
問題文長いので↑で確認してください。。。

Tree.Left, Tree.Rightが*Treeであることに気付けたら、あとは早かったです。

package main

import (
    "fmt"
    "golang.org/x/tour/tree"
)

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
    recursiveWalk(t, ch)
    close(ch)
}

func recursiveWalk(t *tree.Tree, ch chan int) {
    if t.Left != nil {
        recursiveWalk(t.Left, ch)
    }
    ch <- t.Value
    if t.Right != nil {
        recursiveWalk(t.Right, ch)
    }
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go Walk(t1, ch1)
    go Walk(t2, ch2)

    for {
        c1, ok1 := <-ch1
        c2, ok2 := <-ch2
        switch {
        case !ok1, !ok2:
            return ok1 == ok2
        case c1 != c2:
            return false
        }
    }
}

func main() {
    ch := make(chan int)
    go Walk(tree.New(1), ch)
    for c := range ch {
        fmt.Println(c)
    }

    fmt.Println(Same(tree.New(1), tree.New(1)))
    fmt.Println(Same(tree.New(1), tree.New(2)))
}

Exercise: Web Crawler

package main

import (
    "fmt"
    "sync"
    "time"
)

type Fetcher interface {
    // Fetch returns the body of URL and
    // a slice of URLs found on that page.
    Fetch(url string) (body string, urls []string, err error)
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    cacheUrl := make(map[string]int)
    var mux sync.Mutex

    var crawl func(string, int)
    crawl = func(url string, depth int) {
        if depth <= 0 {
            return
        }

        if _, ok := cacheUrl[url]; ok {
            return
        }

        mux.Lock()
        cacheUrl[url]++
        mux.Unlock()
     
        body, urls, err := fetcher.Fetch(url)
        if err != nil {
            fmt.Println(err)
            return
        }

        fmt.Printf("found: %s %q\n", url, body)

        for _, u := range urls {
            go crawl(u, depth-1)
        }

    }

    go crawl(url, depth)
    time.Sleep(time.Second)
    fmt.Println(cacheUrl)
    return
}

func main() {
    Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := f[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
    "https://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "https://golang.org/pkg/",
            "https://golang.org/cmd/",
        },
    },
    "https://golang.org/pkg/": &fakeResult{
        "Packages",
        []string{
            "https://golang.org/",
            "https://golang.org/cmd/",
            "https://golang.org/pkg/fmt/",
            "https://golang.org/pkg/os/",
        },
    },
    "https://golang.org/pkg/fmt/": &fakeResult{
        "Package fmt",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
    "https://golang.org/pkg/os/": &fakeResult{
        "Package os",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
}

合ってるかわからん!
それっぽいはず・・・。

一通り終えて

コードこれで本当に合っているのかはわからないけど、1回目に書いたものということで残しておきます。

これで、みんなのGo言語を読み進めるスタート地点に立てた気がします。。。   本にもあったけど、また慣れてからやれば、もっと違う感じになると思うので、それはまた追々。

とりあえず、終わったよ報告でした。

GoでString to Intしたい

夏季課題として与えられたちょっとしたプログラミングテスト。
で慣れ親しんだJSではなく、わざとGoで挑戦。
せっかく始めたし、知識つけるために。

そんな中、StringをIntにしないといけない場面に出くわして調べた。 (Tour of Goしかやってないぐらいの知識なので)

parseInt的なことやるときはstrconv.Atoi(string)でできることが分かった。
部分的にはこんな場面。

package main

import (
    "fmt"
    "strings"
    "strconv"
)

func main() {
    const S = "20 50 33 50 60"

    numberList := strings.Fields(S)

    sum := 0
    for _, number := range numberList {
        i, _ := strconv.Atoi(number)
        sum += i
    }

    fmt.Println(sum)
}

parseInt的なことやるときはstrconv.Atoi(string)

覚えました。

別言語で当たり前にやってることだけど、こうやってやり方覚えていくのは楽しい。