iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 2
1

今天的主題是 Data Types,雖然乍看之下可能會以為很無聊,但是內容還是很豐富唷!因為我還是會在過程之中介紹到該語言的一些特性。那就讓我們開始吧!


今日挑戰內容

在每個語言定義三個不同 Type 的值,分別是 Integer、Decimal 還有 String,並且另外讓 User 各輸入一個相同 Type 的值,然後相加。這裡 String 的相加指的是把 String 串接起來。

Scala

object Solution {
    def main(args: Array[String]) {
        val i = 4
        val d = 4.0
        val s = "HackerRank "
        println(i + scala.io.StdIn.readLine().toInt)
        println(d + scala.io.StdIn.readLine().toDouble)
        println(s + scala.io.StdIn.readLine())
    }
}
  • 首先來看我們定義變數用的是 val ,這在前一天有提過,如果是用 val 宣告便是 immutable,反之則用 var ,而這裡的 i, d, s 分別是 Int, Double 以及 String,沒有寫出來是因為 Scala 有 Type inference,可以自動從右邊推論出 Type,不然一般來說要寫 val i: Int = 4 。但既然是推論的,有時候難免會有非預期的結果,那麼就應該要自己把 Type 給寫出來囉!
  • Scala 的 Type system 是比較複雜的, Any 是所有 Type 的 Supertype,分別被 AnyValAnyRef 給繼承, AnyVal 又分別有我們常見的 Value types 像是 Double, Float, Long, Int, Short, Byte, Char, Unit, and Boolean ,其中 Unit 只有一個唯一的值就是 () 也就是不帶任何的內容,常用在當我們的 Function 只有 Side effect,但在 Scala 每個 Function 又必須要 Return 時,就會 Return Unit 。而 AnyRef 就是我們自己定義的 Class 或是一些 Scala 內建像是 List, Option 等等。詳細一點的說明可以參考這裡
  • 這裏 scala.io.StdIn.readLine() 所得到的 User input 都是 String,所以假使沒有加上 toInt 或是 toDouble ,會把輸入的內容直接當成字串直接印出來,例如整數的部分輸入 123 ,那麼你會看到印出 4123 ,而非相加的 127
  • 如果我們在程式的最一開始 ( object Solution 之上) 加入 import scala.io.StdIn.readLine ,那麼我們就可以不用每次都寫 scala.io.StdIn.readLine() ,而只要寫 readLine() 了。Scala 的 Import 也沒有規定一定要在最外面,可以在 Object 甚至是 Function 裡面再 import 也行!
  • 這裡有個有趣的是,拿 Int 來說,其實 +Int 這個 Class 的 Method,所以例如 a + b 其實是 a.+(b) ,然而可以寫 a + b 這是 Scala 提供的語法糖囉!

Python 3

i = 4
d = 4.0
s = 'HackerRank '

a = int(input())
b = float(input())
c = str(input())
print('{:d}\n{:.2f}\n{:s}'.format(a+i, b+d, s+c))
  • i, d , s 分別被賦予了 int, float, str 型態的值,這些是 Python 的內建型態 (除此之外還有 bool)。因為 Python 比較麻煩的是,變數並沒有固定的 Type,是跟著被賦予值的型態做變換,但如果我們今天想知道這個變數目前是什麼 Type 或自定義的 Class 的話,可以利用 type 這個函式來幫助我們,例如 print(type(i)) 就會得到 <class 'int'>
  • a, b, c 則是分別透過 Invoke input() 來從 Standard input 讀取 User 所輸入的內容,並且做 Casting,也就是轉換型別。這裡假使不轉換型別的話,當例如字串和整數不小心相加的時候,就會產生 TypeError。而且要注意的是,如果要轉換成例如 int,但是卻輸入像是 abc 的字串,則也會因為無法轉換導致產生 Error 唷!如果要做 Error handling 可以用 try...except 來攔截 Exception 並且進行邏輯上的處理。
  • 最後一行則是在 Python 裡面可以方便用來格式化字串的 Method format。大括號的內容必須從 format 中所對應順序位置的結果而來,例如 {:d} 的值就是 a+i (d 代表是 int), {:.2f} 的值就是 b+d ,這裡的 .2 表示印出來的精準度是小數點後第二位 (ffloat), {:s} 的值是 s+c 。在 Python 的世界,把字串進行相加其實就是進行 concatenation,假設 cis awesome ,那麼 s+c 就是 hackerRank is awesome 囉!

Golang

package main

import (
  "fmt"
  "os"
  "bufio"
  "strconv"
)

func main() {
  
    var i uint64 = 4
    var d float64 = 4.0
    var s string = "HackerRank "

    scanner := bufio.NewScanner(os.Stdin)

    scanner.Scan()
    a, _ := strconv.ParseUint(scanner.Text(),10,64)
    scanner.Scan()
    b, _ := strconv.ParseFloat(scanner.Text(), 64)
    scanner.Scan()
    c := scanner.Text()
    fmt.Println(a+i)    
    fmt.Printf("%.1f\n",b+d)
    fmt.Println(s+c)
  
}
  • 首先我們多引入了 strconv 這個 Package,他的主要功能是讓我們可以把一些基本的 Types 像是整數小數等等和 String 進行互轉。詳細用法與範例可參考這裡
  • 在 Golang 裡頭的基本 Data type 有哪些呢?像是 bool , string , int , int8 , int16 , int32 , int64 , float32 , float64 , uint8 , uint16 , uint32 , uint64 , float32 , float64 ,complex64 , complex128 , byte , rune , uintptr等等。我們可以看到像是 int (整數), float (浮點數), complex (複數) 後面都跟著一些數字像是 32, 64,這些數字代表的是 bit 數,也就是這個值分別是用幾 bit 來儲存。當然 bit 數越大的,能存的數字也就越大囉!其中 byteuint8 的 alias ( u 代表 unsigned) , runeuint32 的 alias (代表一個 Unicode)
  • 接著我們來看到 bufio.NewScanner(os.Stdin) ,還記得之前我們用的是 NewReaderreadLine現在則是用 NewScanner 。首先我們看到官網在 readLine 裡頭提到 ReadLine is a low-level line-reading primitive. Most callers should use ReadBytes(‘\n’) or ReadString(‘\n’) instead or use a Scanner. 所以我們來試試看使用 Scanner 吧!
  • 首先我們透過 bufio.NewScanner(os.Stdin) 得到一個 Scanner,我們到目前為止可以看到不管是 NewReaderNewScanner 的參數都是 os.Stdin ,這到底是什麼呢?如果我們去看官方的文件 (這裡),可以發現 Stdinos Package 的一個 var ,而 Stdin 其實是透過 NewFile 得到一個 type 是 File 的 instance。咦?但是當我們看 NewScanner 的官方文件,需要的參數類型是 io.Reader 呀!於是我們再往下追 (參考這裡),可以看到 io.Reader 是一個 interface。在 Golang 的世界, interface 定義了一些方法,但並沒有實作。如果今天某個 Type 實作了某個 interface 定義的所有方法,例如這裡的 io.Reader 定義了 Read 這個方法,而我們也看到 File 確實實作了 Read (參考這裡)( func (*File) Read),所以我們就可以說 os.Fileio.Reader ,就可以當作參數丟進去給 NewScanner 啦!如果讓我們再繼續深入一點點,可以發現到在 NewScanner 這個 function 裏頭 (定義在這裡),參數 r 會被賦予給 struct Scanner 內的 r ,所以我們可以預期之後應該會有 r.Read 被執行的情況,果不其然在這份 Source code 你可以搜尋到,在 Scan 這個方法裡頭,真的就可以看到 r.Read 囉!假使我們丟入的是 os.File 那麼真正被執行的就是其實作的 Read 也就是 func (*File) Read 。呼!
  • 再來我們看到 scanner.Scan() ,每次執行會讀取一個 token,而 token 默認是讀取一整行,如同我們這邊想要得到的 User input,當然我們也可以自定義一次 token 要怎麼樣讀取 (可以參考這裡)。執行完 Scan() 之後,我們接下來可以看到 scanner.Text() ,這個就是把目前的 token 轉成 string 返回啦!因為每次 Scan() 默認是讀取一行,所以我們如果想要繼續讀下一行,就要再執行一次 Scan() ,這也是為什麼這邊會有三個 Scan() 了。
  • 但由於我們的 var i , var d 分別是 intfloat,所以我們要再經過一次轉換,這裡用到的是 strconv 這個 Package,而我們使用 ParseUint 可以我們把 scanner.Text() 回傳的 string,如果實際上是一個整數的樣子,就可以回傳給我們一個整數,在這裡我們指定 base 為 10,且 bit 是 64,所以我們得到的整數型態是 unit64,就跟 i 一樣。 ParseFloat 也是相同的效果。得到所有我們想要的 User input 之後,就是透過 printlnprintf 把他們相加的結果印出來啦! printf 是讓我們可以格式化輸出,例如這邊的 %.1f 就表示我們希望顯示的精準度是小數點後一位。而由於 printf 不會斷行,所以我們又給了一個 \n

Rust

use std::io::stdin;

fn main() {
    let i: i32 = 4;
    let d: f32 = 4.0;
    let s = "Hackerrack ";

    let mut i_input = String::new();
    let mut d_input = String::new();
    let mut s_input = String::new();

    stdin().read_line(&mut i_input)
      .expect("Failed to read your input");

    let i_input: i32 = match i_input.trim().parse(){
        Ok(num) => num,
        Err(_) => 0
    };

    stdin().read_line(&mut d_input)
        .expect("Failed to read your input");

    let d_input: f32 = match d_input.trim().parse(){
        Ok(num) => num,
        Err(_) => 0.0
    };
  
    stdin().read_line(&mut s_input)
        .expect("Failed to read your input");

    println!("{} + {}> {}", i, i_input,  (i_input + i));
    println!("{:.1} + {:.1}> {:.1}", d, d_input, (d_input + d));
    println!("{} + {}> {} {}", s, s_input, s, s_input);
}
  • Rust 的基本資料型態其實和 Golang 有點像,在整數跟浮點數的部分都會帶 bit 數,例如 i8 , i16 , i32 , i64 , u8 (unsigned integer), u16 , u32 , u64 , f32 (浮點數), f64 ,另外還有 bool , char , str 等等。
  • 這邊我們一樣先定義了 i , d, s 也就是將來要被拿來相加的。接著我們分別定義了 i_input , d_input , s_input 要用來放待會要從 Standard input 得到的字串,記得這邊的 mut 表示我們明確指出這個變數是可以被修改的。而 String::new() 會幫我們產生一個新的空字串。
  • 跟上一篇一樣我們用了 stdin().read_line(&mut input) ,來從 Standard input 讀取字串並且放入 input 之中,為什麼這裡有 &mut 可以回去參考 day 0 的解釋,這是跟所有權以及可變性有關。然而這裡我們多了一個東西 expect("...") ,這是什麼呢?意義上是當 stdin().read_line(&mut input) 有 error 的時候,會印出 expect 中的字串。為什麼可以這樣?其實如果我們去看文件會發現, read_line 回傳的 Type 是 Result ,而 Result 其實是 Rust 裡頭的 Enum type。Enum 就是列舉,但是在 Rust 的 Enum 特別的是,除了列舉不同的 variant 之外,每個 variant 又可以帶值,以 Result 為例 (參考這裡),Result 是一個泛型的 Enum,有兩個 variants, OkErr ,而他們分別有 TE Type 的值。更特別的是,在 Rust 的 Enum 還可以實作方法!而 Result 就實作了 expect 這個方法(這裡),當 ResultErr 的時候,就會印出 expect 中的字串並且結束程式。另外常用的 Enum 還有 Option ,我想我們以後就會遇到了。
  • 再來我們看 let i_input: i32 = match i_input.trim().parse() 這一段。這裡我們首先可以看到變數是可以重新綁定的,雖然我們上面說 i_input 是個字串,但在這邊我們將他重新綁定,且 type 是 i32 。注意,假使 i_input 還是個字串,但你卻想要賦予它一個整數,例如 i_input = 1 ,這樣是不行的唷!再來我們看到出現了 match 這個字, match 可說是相當強大 (Scala 也有 match),不只是可以發揮像是 switch 的功能,還是可以同時幫我們進行解構,這樣講有點抽象 (可參考這裡),我們直接來看這裡做了什麼事。首先 trim() 比較簡單,就是把字串前後的空白給去掉。接著這個字串又呼叫了 parse() ,其作用就是將字串 Parse 並轉換為變數所定義的型別,例如這裡 i_input 定義是 i32 ,那麼 parse() 就會知道要 Parse 出整數且型別是i32 。而 parse() 所回傳的是 Result ,沒錯!就是上面所提過的。再來就換 match 登場啦!這裡 match會幫我們判斷 ResultOk還是 Err 並且做相對應的事。假使 parse() 成功的話,那麼回傳的 Result 就是 Ok(parse出來的值) ,而在 match 中,我們透過給予Ok(num) 這個 Pattern 來把 Parse 出來的值用 num 來存取,你也可以寫 Ok(num) => num + 1 這表示我們把 Parse 出來的值加 1 後再賦予給 i_input 。而如果 Parse() 回傳 Err ,我們就給 i_input一個 Default 值,在這裡是 0 。至於 Err(_)_表示我們並沒有要存取 Err 的值,所以就給一個 _ 表示忽略。而 float 的部分也是相同的概念囉!
  • 最後透過 println 來把結果印出來。其中 {:.1} 表示我們要印的精準度是小數點後第一位囉!

結語

今天雖然本來只是想要介紹資料型態,但是在過程之中,我們又認識了一些語言的特性,所以有時候事情可以看起來很簡單,但是其實很多有趣的東西在裡頭,就看我們願不願意花心思去挖掘囉!明天見!

(今日內容: 8729 個字)

其他文章:
[Day 0]經典的起手式!


上一篇
[Day 0] 最經典的起手式!
下一篇
[Day 2] 你不知道的 Operator!
系列文
30 天把自己榨好榨滿的四週四語言大挑戰!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
sendxph
iT邦新手 5 級 ‧ 2019-10-03 17:28:20

您好,

Golang 部份似乎誤植了 day0 的 code 了....
謝謝您的好文章

已修正,感謝提醒!

我要留言

立即登入留言