Thành ngữ Go tương đương với toán tử ternary của C là gì?


297

Trong C / C ++ (và nhiều ngôn ngữ của họ đó), một thành ngữ phổ biến để khai báo và khởi tạo một biến tùy thuộc vào một điều kiện sử dụng toán tử điều kiện ternary:

int index = val > 0 ? val : -val

Go không có toán tử điều kiện. Cách thành ngữ nhất để thực hiện cùng một đoạn mã như trên là gì? Tôi đã đi đến giải pháp sau đây, nhưng có vẻ khá dài dòng

var index int

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

Có điều gì tốt hơn?


bạn có thể khởi tạo giá trị với phần khác và chỉ kiểm tra xem điều kiện của bạn có thay đổi hay không, mặc dù vậy nó không tốt hơn
x29a

Dù sao thì rất nhiều if / thens đã bị loại bỏ. Chúng tôi thường làm điều này mọi lúc kể từ những ngày tôi viết chương trình BASIC đầu tiên của mình cách đây 35 năm. Ví dụ của bạn có thể là: int index = -val + 2 * val * (val > 0);
hyc

9
@hyc ví dụ của bạn không thể đọc được như mã thành ngữ của go, hoặc thậm chí là phiên bản của C bằng cách sử dụng toán tử ternary. Dù sao, AFAIK, không thể thực hiện giải pháp này trong Go vì boolean không thể được sử dụng làm giá trị số.
Fabien

Tự hỏi tại sao đi không cung cấp một nhà điều hành như vậy?
Eric Wang

@EricWang Hai lý do, AFAIK: 1- bạn không cần nó và họ muốn giữ ngôn ngữ càng nhỏ càng tốt. 2- nó có xu hướng bị lạm dụng, tức là được sử dụng trong các biểu thức nhiều dòng phức tạp và các nhà thiết kế ngôn ngữ không thích nó.
Fabien

Câu trả lời:


244

Như đã chỉ ra (và hy vọng không có gì đáng ngạc nhiên), sử dụng if+elsethực sự là cách thành ngữ để làm điều kiện trong Go.

Tuy nhiên, ngoài var+if+elsekhối mã đầy đủ , chính tả này cũng được sử dụng thường xuyên:

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

và nếu bạn có một khối mã đủ lặp đi lặp lại, chẳng hạn như tương đương int value = a <= b ? a : b, bạn có thể tạo một hàm để giữ nó:

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

...

value := min(a, b)

Trình biên dịch sẽ nội tuyến các hàm đơn giản như vậy, vì vậy nó nhanh, rõ hơn và ngắn hơn.


184
Này các bạn, nhìn kìa! Tôi chỉ chuyển nhà điều hành ternarity cho các golang! play.golang.org/p/ZgLwC_DHm0 . Thật hiệu quả!
thwd

28
@tomwilde giải pháp của bạn trông khá thú vị, nhưng nó thiếu một trong những tính năng chính của toán tử ternary - đánh giá có điều kiện.
Vladimir Matveev

12
@VladimirMatveev bọc các giá trị trong các bao đóng;)
nemo

55
c := (map[bool]int{true: a, false: a - 1})[a > b]là một ví dụ về IMHO obfuscation, ngay cả khi nó hoạt động.
Rick-777

34
Nếu if/elselà cách tiếp cận thành ngữ thì có lẽ Golang có thể xem xét cho phép if/elsecác mệnh đề trả về một giá trị : x = if a {1} else {0}. Go sẽ không phải là ngôn ngữ duy nhất để làm việc theo cách này. Một ví dụ chính là Scala. Xem: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy

80

No Go không có toán tử ternary, sử dụng cú pháp if / other cách thành ngữ.

Tại sao Go không có toán tử ?:?

Không có hoạt động kiểm tra ternary trong Go. Bạn có thể sử dụng những điều sau đây để đạt được kết quả tương tự:

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

Lý do ?:vắng mặt ở Go là các nhà thiết kế của ngôn ngữ đã thấy hoạt động được sử dụng quá thường xuyên để tạo ra các biểu thức phức tạp không thể chối cãi. Các if-elsehình thức, mặc dù dài hơn, rõ ràng là rõ ràng hơn. Một ngôn ngữ chỉ cần một cấu trúc luồng điều khiển có điều kiện.

- Câu hỏi thường gặp (FAQ) - Ngôn ngữ lập trình Go


1
Vì vậy, chỉ vì những gì các nhà thiết kế ngôn ngữ đã thấy, họ đã bỏ qua một lớp lót cho cả một if-elsekhối? Và ai nói if-elsekhông bị lạm dụng theo cách tương tự? Tôi không tấn công bạn, tôi chỉ cảm thấy rằng cái cớ của các nhà thiết kế không đủ hợp lệ
Alf Moh

58

Giả sử bạn có biểu thức ternary sau (bằng C):

int a = test ? 1 : 2;

Cách tiếp cận thành ngữ trong Go sẽ chỉ đơn giản là sử dụng một ifkhối:

var a int

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

Tuy nhiên, điều đó có thể không phù hợp với yêu cầu của bạn. Trong trường hợp của tôi, tôi cần một biểu thức nội tuyến cho một mẫu tạo mã.

Tôi đã sử dụng một chức năng ẩn danh được đánh giá ngay lập tức:

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

Điều này đảm bảo rằng cả hai chi nhánh không được đánh giá là tốt.


Điều tốt để biết rằng chỉ có một nhánh của hàm anon nội tuyến được đánh giá. Nhưng lưu ý rằng những trường hợp như thế này nằm ngoài phạm vi của toán tử tạm thời của C.
Sói

1
Biểu thức điều kiện C (thường được gọi là toán tử ternary) có ba toán hạng : expr1 ? expr2 : expr3. Nếu expr1đánh giá true, expr2được đánh giá và là kết quả của biểu thức. Mặt khác, expr3được đánh giá và cung cấp như là kết quả. Đây là từ Ngôn ngữ lập trình ANSI C phần 2.11 của K & R. Giải pháp My Go bảo tồn các ngữ nghĩa cụ thể này. @Wolf Bạn có thể làm rõ những gì bạn đang đề xuất?
Peter Boyer

Tôi không chắc những gì tôi đã nghĩ, có lẽ các hàm anon cung cấp một phạm vi (không gian tên cục bộ) không phải là trường hợp với toán tử ternary trong C / C ++. Xem ví dụ để sử dụng phạm vi này
Sói

39

Ternary bản đồ rất dễ đọc mà không cần dấu ngoặc đơn:

c := map[bool]int{true: 1, false: 0} [5 > 4]

Không hoàn toàn chắc chắn lý do tại sao nó có -2 ... vâng, nó là một cách giải quyết nhưng nó hoạt động và an toàn kiểu.
Alessandro Santini

30
Vâng, nó hoạt động, là loại an toàn, và thậm chí là sáng tạo; tuy nhiên, có những số liệu khác. Op ternary là thời gian chạy tương đương với if / other (xem ví dụ bài đăng S / O này ). Phản hồi này không phải vì 1) cả hai nhánh được thực thi, 2) tạo bản đồ 3) gọi hàm băm. Tất cả những thứ này đều "nhanh", nhưng không nhanh bằng if / other. Ngoài ra, tôi sẽ lập luận rằng nó không dễ đọc hơn var r T nếu điều kiện {r = foo ()} khác {r = bar ()}
hiệp sĩ

Trong các ngôn ngữ khác, tôi sử dụng phương pháp này khi tôi có nhiều biến và với các bao đóng hoặc hàm con trỏ hoặc hàm nhảy. Viết ifs lồng nhau trở nên dễ bị lỗi khi số lượng biến tăng, trong khi đó, ví dụ {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (mã giả) ngày càng hấp dẫn hơn khi số lượng biến tăng lên. Việc đóng cửa giữ cho mô hình này nhanh chóng. Tôi hy vọng rằng sự đánh đổi tương tự được áp dụng trong đi.
Tối đa Murphy

Tôi cho rằng trong lúc bạn sẽ sử dụng một công tắc cho mô hình đó. Tôi thích cách chuyển mạch tự động ngắt, ngay cả khi đôi khi nó bất tiện.
Tối đa Murphy

8
như Cassy Foesch đã chỉ ra: simple and clear code is better than creative code.
Sói

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)
}

Điều này sẽ không tốt hơn nếu / khác và yêu cầu diễn viên nhưng hoạt động. Tài chính

Điểm chuẩnAbsTernary-8 100000000 18,8 ns / op

Điểm chuẩnAbs IfElse-8 2000000000 0,27 ns / op


Đây là giải pháp tốt nhất, xin chúc mừng! Một dòng xử lý tất cả các trường hợp có thể xảy ra
Alexandro de Oliveira

2
Tôi không nghĩ rằng điều này xử lý đánh giá có điều kiện, hoặc nó? Với các nhánh miễn phí có tác dụng phụ, điều này không thành vấn đề (như trong ví dụ của bạn), nhưng nếu đó là thứ gì đó có tác dụng phụ thì bạn sẽ gặp vấn đề.
Ashton Wiersdorf

7

Nếu tất cả các chi nhánh của bạn tạo ra tác dụng phụ hoặc đắt tiền về mặt tính toán thì những điều sau đây sẽ là tái cấu trúc bảo tồn về mặt ngữ nghĩa :

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

thông thường không có chi phí hoạt động (nội tuyến) và quan trọng nhất là không làm lộn xộn không gian tên của bạn với các hàm trợ giúp chỉ được sử dụng một lần (làm cản trở khả năng đọc và bảo trì). Ví dụ trực tiếp

Lưu ý nếu bạn ngây thơ áp dụng cách tiếp cận của Gustavo :

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

bạn sẽ nhận được một chương trình với một hành vi khác nhau ; trong trường hợp val <= 0chương trình sẽ in một giá trị không tích cực trong khi nó không nên! (Tương tự, nếu bạn đảo ngược các nhánh, bạn sẽ giới thiệu chi phí bằng cách gọi một chức năng chậm không cần thiết.)


1
Thú vị đọc, nhưng tôi không thực sự hiểu điểm trong những lời chỉ trích của bạn về cách tiếp cận của Gustavo. Tôi thấy một hàm (loại) abstrong mã gốc (tốt, tôi sẽ đổi <=thành <). Trong ví dụ của bạn, tôi thấy một khởi tạo, trong một số trường hợp là dư thừa và có thể mở rộng. Bạn có thể vui lòng làm rõ: giải thích ý tưởng của bạn nhiều hơn một chút?
Sói

Sự khác biệt chính là việc gọi một chức năng bên ngoài một trong hai nhánh sẽ tạo ra tác dụng phụ ngay cả khi nhánh đó không nên được thực hiện. Trong trường hợp của tôi, chỉ có số dương sẽ được in vì hàm printPositiveAndReturnchỉ được gọi cho số dương. Ngược lại, luôn luôn thực thi một nhánh, sau đó "sửa" giá trị bằng cách thực thi một nhánh khác không hoàn tác các tác dụng phụ của nhánh đầu tiên .
eold

Tôi thấy, nhưng kinh nghiệm lập trình viên thường nhận thức được tác dụng phụ. Trong trường hợp đó, tôi thích giải pháp rõ ràng của Cassy Foesch cho một hàm nhúng, ngay cả khi mã được biên dịch có thể giống nhau: nó ngắn hơn và có vẻ rõ ràng đối với hầu hết các lập trình viên. Đừng hiểu lầm tôi: Tôi thực sự yêu thích sự khép kín của Go;)
Wolf

1
"Các lập trình viên kinh nghiệm thường nhận thức được các tác dụng phụ " - Không. Việc tránh đánh giá các thuật ngữ là một trong những đặc điểm chính của một nhà điều hành ternary.
Jonathan Hartley

6

Lời nói đầu: Không cần bàn cãi rằngif else là cách để đi, chúng ta vẫn có thể chơi và tìm thấy niềm vui trong các cấu trúc kích hoạt ngôn ngữ.

Cấu Iftrúc sau đây có sẵn trong github.com/icza/goxthư viện của tôi với nhiều phương thức khác, là builtinx.Ifkiểu.


Go cho phép đính kèm các phương thức vào bất kỳ loại do người dùng xác định , bao gồm các kiểu nguyên thủy như bool. Chúng ta có thể tạo một loại tùy chỉnh có kiểu cơ bảnbool của nó và sau đó với một chuyển đổi loại đơn giản với điều kiện, chúng ta có quyền truy cập vào các phương thức của nó. Các phương thức nhận và chọn từ các toán hạng.

Một cái gì đó như thế này:

type If bool

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

Làm thế nào chúng ta có thể sử dụng nó?

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

Ví dụ: một ternary làm max():

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

Một ternary làm abs():

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

Điều này có vẻ mát mẻ, nó đơn giản, thanh lịch và hiệu quả (nó cũng đủ điều kiện để nội tuyến ).

Một nhược điểm so với toán tử ternary "thực": nó luôn đánh giá tất cả các toán hạng.

Để đạt được đánh giá hoãn lại và chỉ khi cần thiết, tùy chọn duy nhất là sử dụng các hàm (có thể là hàm hoặc phương thức được khai báo hoặc bằng chữ ), chỉ được gọi khi / nếu cần:

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

Sử dụng nó: Giả sử chúng ta có các hàm này để tính toán ab:

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

Sau đó:

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

Ví dụ: điều kiện là năm hiện tại> 2020:

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

Nếu chúng ta muốn sử dụng hàm chữ:

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

Lưu ý cuối cùng: nếu bạn có các chức năng với các chữ ký khác nhau, bạn không thể sử dụng chúng ở đây. Trong trường hợp đó, bạn có thể sử dụng một chức năng bằng chữ với chữ ký phù hợp để làm cho chúng vẫn có thể áp dụng được.

Ví dụ: nếu calca()calcb()cũng sẽ có các tham số (bên cạnh giá trị trả về):

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

Đây là cách bạn có thể sử dụng chúng:

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

Hãy thử những ví dụ trên Sân chơi Go .


4

Câu trả lời của eold là thú vị và sáng tạo, thậm chí có thể thông minh.

Tuy nhiên, thay vào đó, nên làm:

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

Có, cả hai đều biên dịch về cơ bản cùng một tổ hợp, tuy nhiên mã này dễ đọc hơn nhiều so với việc gọi một hàm ẩn danh chỉ để trả về một giá trị có thể được ghi vào biến ở vị trí đầu tiên.

Về cơ bản, mã đơn giản và rõ ràng tốt hơn mã sáng tạo.

Ngoài ra, bất kỳ mã nào sử dụng một bản đồ theo nghĩa đen không phải là một ý tưởng hay, bởi vì bản đồ không hề nhẹ chút nào trong Go. Kể từ Go 1.3, thứ tự lặp ngẫu nhiên cho các bản đồ nhỏ được đảm bảo và để thực thi điều này, nó đã trở nên kém hiệu quả hơn về bộ nhớ đối với các bản đồ nhỏ.

Do đó, việc tạo và xóa nhiều bản đồ nhỏ vừa tốn không gian vừa tốn thời gian. Tôi đã có một đoạn mã sử dụng một bản đồ nhỏ (có thể có hai hoặc ba khóa, nhưng trường hợp sử dụng phổ biến chỉ là một mục nhập) Nhưng mã bị chậm. Chúng ta đang nói chậm hơn ít nhất 3 bậc độ lớn so với cùng một mã được viết lại để sử dụng bản đồ khóa kép [index] => data [index]. Và có khả năng là nhiều hơn. Vì một số thao tác trước đây mất vài phút để chạy, đã bắt đầu hoàn thành sau mili giây. \


1
simple and clear code is better than creative code- điều này tôi rất thích, nhưng tôi cảm thấy hơi bối rối trong phần cuối cùng sau đó dog slow, có lẽ điều này cũng có thể gây nhầm lẫn cho người khác?
Sói

1
Vì vậy, về cơ bản ... tôi có một số mã đang tạo các bản đồ nhỏ với một, hai hoặc ba mục, nhưng mã đang chạy rất chậm. Vì vậy, rất nhiều m := map[string]interface{} { a: 42, b: "stuff" }, và sau đó trong một chức năng khác lặp qua nó: for key, val := range m { code here } Sau khi chuyển sang hệ thống hai lát: keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }và sau đó lặp đi lặp lại như for i, key := range keys { val := data[i] ; code here }những thứ tăng tốc 1000 lần.
Cassy Foesch

Tôi thấy, cảm ơn đã làm rõ. (Có lẽ bản thân câu trả lời có thể được cải thiện vào thời điểm này.)
Wolf

1
-.- ... touché, logic ... touché ... cuối cùng tôi cũng sẽ hiểu điều đó ...;)
Cassy Foesch

3

Một lớp lót, mặc dù bị những người sáng tạo xa lánh, có chỗ đứng của họ.

Điều này giải quyết vấn đề đánh giá lười biếng bằng cách cho phép bạn, tùy ý, vượt qua các chức năng được đánh giá nếu cần thiết:

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)())
}

Đầu ra

hello
hello world
hello world
hello
bye
  • Các chức năng được truyền vào phải trả về một interface{}để thỏa mãn thao tác truyền nội bộ.
  • Tùy thuộc vào ngữ cảnh, bạn có thể chọn chuyển đầu ra thành một loại cụ thể.
  • Nếu bạn muốn trả về một hàm từ cái này, bạn sẽ cần phải bọc nó như hiển thị với c.

Giải pháp độc lập ở đây cũng tốt, nhưng có thể ít rõ ràng hơn đối với một số mục đích sử dụng.


Ngay cả khi điều này chắc chắn không phải là học thuật, điều này là khá tốt đẹp.
Fabien
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.