Apa yang setara idiomatik Go dari operator ternary C?

297

Dalam C / C ++ (dan banyak bahasa dari keluarga itu), idiom umum untuk menyatakan dan menginisialisasi variabel tergantung pada suatu kondisi menggunakan operator kondisional ternary:

int index = val > 0 ? val : -val

Go tidak memiliki operator kondisional. Apa cara paling idiomatis untuk mengimplementasikan kode yang sama seperti di atas? Saya datang ke solusi berikut, tetapi sepertinya cukup bertele-tele

var index int

if val > 0 {
    index = val
} else {
    index = -val
}

Apakah ada sesuatu yang lebih baik?

Fabien
sumber
Anda dapat menginisialisasi nilai dengan bagian lain dan hanya memeriksa kondisi Anda untuk berubah, tidak yakin itu lebih baik
x29a
Banyak jika / seharusnya dihilangkan pula. Kami biasa melakukan ini sepanjang waktu sejak saya menulis program BASIC pertama saya 35 tahun yang lalu. Contoh Anda dapat: int index = -val + 2 * val * (val > 0);
hyc
9
@ hyc contoh Anda jauh dari mudah dibaca sebagai kode idiomatik go, atau bahkan versi C menggunakan operator ternary. Pokoknya, AFAIK, tidak mungkin untuk mengimplementasikan solusi ini di Go karena boolean tidak dapat digunakan sebagai nilai numerik.
Fabien
Ingin tahu mengapa pergi tidak menyediakan operator seperti itu?
Eric Wang
@EricWang Dua alasan, AFAIK: 1- Anda tidak membutuhkannya, dan mereka ingin menjaga bahasa sekecil mungkin. 2- itu cenderung disalahgunakan, yaitu digunakan dalam ekspresi multi-baris berbelit-belit, dan desainer bahasa tidak menyukainya.
Fabien

Jawaban:

244

Seperti yang ditunjukkan (dan mudah-mudahan tidak mengejutkan), menggunakan if+elsememang cara idiomatis untuk melakukan persyaratan di Go.

Selain var+if+elseblok kode yang penuh, ejaan ini juga sering digunakan:

index := val
if val <= 0 {
    index = -val
}

dan jika Anda memiliki blok kode yang cukup berulang, seperti yang setara int value = a <= b ? a : b, Anda dapat membuat fungsi untuk menahannya:

func min(a, b int) int {
    if a <= b {
        return a
    }
    return b
}

...

value := min(a, b)

Kompiler akan menampilkan fungsi-fungsi sederhana seperti itu, sehingga cepat, lebih jelas, dan lebih pendek.

Gustavo Niemeyer
sumber
184
Hai teman-teman, lihat! Saya baru saja mengirim operator ternarity ke golang! play.golang.org/p/ZgLwC_DHm0 . Sangat efisien!
thwd
28
@tomwilde solusi Anda terlihat cukup menarik, tetapi tidak memiliki salah satu fitur utama dari operator ternary - evaluasi bersyarat.
Vladimir Matveev
12
@VladimirMatveev membungkus nilai dalam penutupan;)
nemo
55
c := (map[bool]int{true: a, false: a - 1})[a > b]adalah contoh dari IMHO kebingungan, bahkan jika itu berhasil.
Rick-777
34
Jika if/elseadalah pendekatan idiomatik maka mungkin Golang bisa mempertimbangkan membiarkan if/elseklausa mengembalikan nilai: x = if a {1} else {0}. Go tidak akan berarti satu-satunya bahasa yang bekerja dengan cara ini. Contoh utama adalah Scala. Lihat: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy
80

No Go tidak memiliki operator ternary, menggunakan sintaks if / else adalah cara idiomatis.

Mengapa Go tidak memiliki operator:?

Tidak ada operasi pengujian ternary di Go. Anda dapat menggunakan yang berikut ini untuk mencapai hasil yang sama:

if expr {
    n = trueVal
} else {
    n = falseVal
}

Alasannya ?:absen dari Go adalah bahwa perancang bahasa telah melihat operasi yang digunakan terlalu sering untuk membuat ekspresi rumit yang tak dapat ditembus. The if-elsebentuk, meskipun lagi, tidak diragukan lagi lebih jelas. Bahasa hanya membutuhkan satu konstruksi aliran kontrol bersyarat.

- Pertanyaan yang Sering Diajukan (FAQ) - Bahasa Pemrograman Go

ishaaq
sumber
1
Jadi hanya karena apa yang telah dilihat oleh perancang bahasa, mereka telah menghilangkan satu baris untuk satu if-elseblok penuh? Dan siapa bilang if-elsetidak disalahgunakan dengan cara yang sama? Saya tidak menyerang Anda, saya hanya merasa bahwa alasan oleh para desainer tidak cukup valid
Alf Moh
58

Misalkan Anda memiliki ekspresi ternary berikut (dalam C):

int a = test ? 1 : 2;

Pendekatan idiomatis di Go adalah dengan menggunakan ifblok:

var a int

if test {
  a = 1
} else {
  a = 2
}

Namun, itu mungkin tidak sesuai dengan kebutuhan Anda. Dalam kasus saya, saya membutuhkan ekspresi sebaris untuk templat pembuatan kode.

Saya menggunakan fungsi anonim yang langsung dievaluasi:

a := func() int { if test { return 1 } else { return 2 } }()

Ini memastikan bahwa kedua cabang tidak dievaluasi juga.

Peter Boyer
sumber
Baik untuk mengetahui bahwa hanya satu cabang dari fungsi anon sebaris yang dievaluasi. Tetapi perhatikan bahwa kasus seperti ini berada di luar cakupan operator ternary C.
Wolf
1
C ekspresi kondisional (umumnya dikenal sebagai operator ternary) memiliki tiga operan: expr1 ? expr2 : expr3. Jika expr1dievaluasi ke true, expr2dievaluasi dan merupakan hasil dari ekspresi. Kalau tidak, expr3dievaluasi dan diberikan hasilnya. Ini dari Bahasa Pemrograman ANSI C bagian 2.11 oleh K&R. Solusi My Go mempertahankan semantik khusus ini. @ Serigala Bisakah Anda mengklarifikasi apa yang Anda sarankan?
Peter Boyer
Saya tidak yakin apa yang ada dalam pikiran saya, mungkin fungsi anon menyediakan ruang lingkup (namespace lokal) yang tidak terjadi dengan operator ternary di C / C ++. Lihat contoh untuk menggunakan lingkup ini
Wolf
39

Peta ternary mudah dibaca tanpa tanda kurung:

c := map[bool]int{true: 1, false: 0} [5 > 4]
pengguna1212212
sumber
Tidak sepenuhnya yakin mengapa ia mendapat -2 ... ya, itu adalah solusi tetapi ia bekerja dan tipe-aman.
Alessandro Santini
30
Ya, itu berfungsi, aman untuk mengetik, dan bahkan kreatif; Namun, ada metrik lainnya. Ops ternary setara runtime dengan if / else (lihat misalnya posting S / O ini ). Tanggapan ini bukan karena 1) kedua cabang dieksekusi, 2) membuat peta 3) panggilan hash. Semua ini "cepat", tetapi tidak secepat jika / yang lain. Juga, saya berpendapat bahwa itu tidak lebih mudah dibaca daripada var r T jika kondisi {r = foo ()} else {r = bar ()}
knight
Dalam bahasa lain saya menggunakan pendekatan ini ketika saya memiliki beberapa variabel dan dengan penutupan atau fungsi pointer atau melompat. Menulis jika bersarang menjadi rawan kesalahan karena jumlah variabel meningkat, sedangkan misalnya {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (pseudocode) menjadi semakin menarik ketika jumlah variabel naik. Penutupan membuat model ini cepat. Saya berharap pengorbanan serupa berlaku di go.
Max Murphy
Saya kira Anda akan menggunakan saklar untuk model itu. Saya suka cara go switch rusak secara otomatis, bahkan jika kadang-kadang tidak nyaman.
Max Murphy
8
seperti yang ditunjukkan Cassy Foesch: simple and clear code is better than creative code.
Wolf
11
func Ternary(statement bool, a, b interface{}) interface{} {
    if statement {
        return a
    }
    return b
}

func Abs(n int) int {
    return Ternary(n >= 0, n, -n).(int)
}

Ini tidak akan mengungguli jika / else dan membutuhkan pemain tetapi berfungsi. FYI:

BenchmarkAbsTernary-8 100000000 18,8 ns / op

BenchmarkAbsIfElse-8 2000000000 0,27 ns / op

Phillip Dominy
sumber
Ini solusi terbaik, selamat! Satu baris yang menangani semua kasus yang mungkin
Alexandro de Oliveira
2
Saya tidak berpikir ini menangani evaluasi bersyarat, atau apakah itu? Dengan cabang bebas efek samping, ini tidak masalah (seperti pada contoh Anda), tetapi jika ada efek sampingnya, Anda akan mengalami masalah.
Ashton Wiersdorf
7

Jika semua cabang Anda membuat efek samping atau mahal secara komputasi , berikut ini akan menjadi refactoring yang melestarikan secara semantik :

index := func() int {
    if val > 0 {
        return printPositiveAndReturn(val)
    } else {
        return slowlyReturn(-val)  // or slowlyNegate(val)
    }
}();  # exactly one branch will be evaluated

dengan biasanya tidak ada overhead (sebaris) dan, yang paling penting, tanpa mengacaukan namespace Anda dengan fungsi pembantu yang hanya digunakan sekali (yang menghambat keterbacaan dan pemeliharaan). Contoh Langsung

Perhatikan jika Anda secara naif menerapkan pendekatan Gustavo :

    index := printPositiveAndReturn(val);
    if val <= 0 {
        index = slowlyReturn(-val);  // or slowlyNegate(val)
    }

Anda akan mendapatkan program dengan perilaku yang berbeda ; dalam hal val <= 0program akan mencetak nilai non-positif sedangkan seharusnya tidak! (Secara analogi, jika Anda membalikkan cabang, Anda akan memperkenalkan overhead dengan memanggil fungsi yang lambat tidak perlu.)

Eold
sumber
1
Menarik dibaca, tapi saya tidak benar-benar memahami poin kritik Anda terhadap pendekatan Gustavo. Saya melihat fungsi (semacam) absdalam kode asli (well, saya akan berubah <=menjadi <). Dalam contoh Anda, saya melihat inisialisasi, yang berlebihan dalam beberapa kasus dan bisa ekspansif. Bisakah Anda menjelaskan: jelaskan ide Anda lebih banyak?
Wolf
Perbedaan utama adalah bahwa memanggil fungsi di luar cabang mana pun akan membuat efek samping bahkan jika cabang itu tidak diambil. Dalam kasus saya, hanya angka positif yang akan dicetak karena fungsinya printPositiveAndReturnhanya dipanggil untuk angka positif. Sebaliknya, selalu mengeksekusi satu cabang, lalu "memperbaiki" nilai dengan mengeksekusi cabang yang berbeda tidak membatalkan efek samping cabang pertama .
diceritakan
Saya mengerti, tetapi para pemrogram yang berpengalaman biasanya menyadari efek sampingnya. Dalam hal ini saya lebih suka solusi nyata Cassy Foesch untuk fungsi tertanam, bahkan jika kode yang dikompilasi mungkin sama: lebih pendek dan terlihat jelas bagi sebagian besar programmer. Jangan salah paham: Saya sangat suka penutupan Go;)
Wolf
1
" programmer berpengalaman biasanya menyadari efek samping " - Tidak. Menghindari evaluasi istilah adalah salah satu karakteristik utama dari operator ternary.
Jonathan Hartley
6

Kata Pengantar: Tanpa berdebat bahwa if elseini adalah jalan yang harus ditempuh, kita masih dapat bermain dan menemukan kesenangan dalam konstruksi yang didukung bahasa.

IfKonstruk berikut tersedia di github.com/icza/goxperpustakaan saya dengan banyak metode lain, sebagai builtinx.Iftipenya.


Go memungkinkan untuk melampirkan metode ke tipe yang ditentukan pengguna , termasuk tipe primitif seperti bool. Kita dapat membuat tipe kustom yang memiliki boolsebagai tipe yang mendasarinya , dan kemudian dengan konversi tipe sederhana pada kondisi tersebut, kita memiliki akses ke metode-metodenya. Metode yang menerima dan memilih dari operan.

Sesuatu seperti ini:

type If bool

func (c If) Int(a, b int) int {
    if c {
        return a
    }
    return b
}

Bagaimana kita bisa menggunakannya?

i := If(condition).Int(val1, val2)  // Short variable declaration, i is of type int
     |-----------|  \
   type conversion   \---method call

Misalnya seorang ternary melakukan max():

i := If(a > b).Int(a, b)

Seorang ternary melakukan abs():

i := If(a >= 0).Int(a, -a)

Ini terlihat keren, sederhana, elegan, dan efisien (ini juga memenuhi syarat untuk inlining ).

Satu kelemahan dibandingkan dengan operator ternary "nyata": selalu mengevaluasi semua operan.

Untuk mencapai evaluasi yang ditangguhkan dan hanya-jika-diperlukan, satu-satunya pilihan adalah menggunakan fungsi (baik fungsi atau metode yang dideklarasikan , atau fungsi literal ), yang hanya dipanggil ketika / jika diperlukan:

func (c If) Fint(fa, fb func() int) int {
    if c {
        return fa()
    }
    return fb()
}

Menggunakannya: Mari kita asumsikan kita memiliki fungsi-fungsi ini untuk menghitung adan b:

func calca() int { return 3 }
func calcb() int { return 4 }

Kemudian:

i := If(someCondition).Fint(calca, calcb)

Misalnya, kondisi menjadi tahun berjalan> 2020:

i := If(time.Now().Year() > 2020).Fint(calca, calcb)

Jika kita ingin menggunakan fungsi literal:

i := If(time.Now().Year() > 2020).Fint(
    func() int { return 3 },
    func() int { return 4 },
)

Catatan akhir: jika Anda akan memiliki fungsi dengan tanda tangan yang berbeda, Anda tidak dapat menggunakannya di sini. Dalam hal ini Anda dapat menggunakan fungsi literal dengan tanda tangan yang cocok untuk membuatnya masih berlaku.

Misalnya jika calca()dan calcb()akan memiliki parameter juga (selain nilai balik):

func calca2(x int) int { return 3 }
func calcb2(x int) int { return 4 }

Ini adalah bagaimana Anda dapat menggunakannya:

i := If(time.Now().Year() > 2020).Fint(
    func() int { return calca2(0) },
    func() int { return calcb2(0) },
)

Cobalah contoh-contoh ini di Go Playground .

icza
sumber
4

jawaban eold menarik dan kreatif, bahkan mungkin pintar.

Namun, disarankan untuk melakukan:

var index int
if val > 0 {
    index = printPositiveAndReturn(val)
} else {
    index = slowlyReturn(-val)  // or slowlyNegate(val)
}

Ya, keduanya mengkompilasi pada dasarnya rakitan yang sama, namun kode ini jauh lebih mudah dibaca daripada memanggil fungsi anonim hanya untuk mengembalikan nilai yang bisa ditulis ke variabel di tempat pertama.

Pada dasarnya, kode sederhana dan jelas lebih baik daripada kode kreatif.

Selain itu, kode apa pun yang menggunakan peta literal bukan ide yang baik, karena peta tidak ringan sama sekali di Go. Sejak Go 1.3, urutan iterasi acak untuk peta kecil dijamin, dan untuk menegakkan ini, memori yang didapat agak kurang efisien untuk peta kecil.

Akibatnya, membuat dan menghapus banyak peta kecil memakan banyak ruang dan waktu. Saya memiliki sepotong kode yang menggunakan peta kecil (dua atau tiga kunci, mungkin, tetapi kasus penggunaan umum hanya satu entri) Tetapi kode itu lambat anjing. Kita berbicara setidaknya 3 urutan besarnya lebih lambat dari kode yang sama ditulis ulang untuk menggunakan kunci slice ganda [indeks] => data peta [indeks]. Dan kemungkinan lebih banyak. Karena beberapa operasi yang sebelumnya membutuhkan waktu beberapa menit untuk dijalankan, mulai menyelesaikan dalam milidetik. \

Cassy Foesch
sumber
1
simple and clear code is better than creative code- ini saya sangat suka, tapi saya agak bingung di bagian terakhir setelah itu dog slow, mungkin ini bisa membingungkan orang lain juga?
Wolf
1
Jadi, pada dasarnya ... Saya punya beberapa kode yang membuat peta kecil dengan satu, dua, atau tiga entri, tetapi kode itu berjalan sangat lambat. Jadi, banyak m := map[string]interface{} { a: 42, b: "stuff" }, dan kemudian di fungsi lain iterasi melalui itu: for key, val := range m { code here } Setelah beralih ke sistem dua slice:, keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }dan kemudian beralih melalui for i, key := range keys { val := data[i] ; code here }hal - hal seperti dipercepat 1000 kali lipat.
Cassy Foesch
Begitu ya, terima kasih atas klarifikasi. (Mungkin jawabannya sendiri dapat ditingkatkan pada titik ini.)
Wolf
1
-.- ... sentuhan, logika ... sentuhan ... Saya akan membahasnya pada akhirnya ...;)
Cassy Foesch
3

One-liner, meskipun dijauhi oleh pencipta, memiliki tempat mereka.

Yang ini menyelesaikan masalah evaluasi malas dengan membiarkan Anda, secara opsional, melewati fungsi yang akan dievaluasi jika perlu:

func FullTernary(e bool, a, b interface{}) interface{} {
    if e {
        if reflect.TypeOf(a).Kind() == reflect.Func {
            return a.(func() interface{})()
        }
        return a
    }
    if reflect.TypeOf(b).Kind() == reflect.Func {
        return b.(func() interface{})()
    }
    return b
}

func demo() {
    a := "hello"
    b := func() interface{} { return a + " world" }
    c := func() interface{} { return func() string { return "bye" } }
    fmt.Println(FullTernary(true, a, b).(string)) // cast shown, but not required
    fmt.Println(FullTernary(false, a, b))
    fmt.Println(FullTernary(true, b, a))
    fmt.Println(FullTernary(false, b, a))
    fmt.Println(FullTernary(true, c, nil).(func() string)())
}

Keluaran

hello
hello world
hello world
hello
bye
  • Fungsi yang dilewati harus mengembalikan suatu interface{}untuk memenuhi operasi pemeran internal.
  • Bergantung pada konteksnya, Anda dapat memilih untuk menampilkan output ke tipe tertentu.
  • Jika Anda ingin mengembalikan fungsi dari ini, Anda harus membungkusnya seperti yang ditunjukkan pada c.

Solusi mandiri di sini juga bagus, tetapi bisa jadi kurang jelas untuk beberapa kegunaan.

bangsawan
sumber
Bahkan jika ini jelas bukan akademik, ini cukup bagus.
Fabien