סוג נתונים מגדיר אילו ערכים משתנה יכול להחזיק ואילו פעולות אפשר לבצע עליהם. זה נשמע כמו נושא בסיסי, אבל אותן פעולות עשויות להתנהג בצורה שונה מאוד בין Java, TypeScript ו-Python.
לדוגמה, `+` מחבר מספרים:
5 + 4 = 9אבל עם מחרוזות הוא מחבר טקסט:
"אב" + "ג" = "אבג"לכן לא מספיק לדעת מה כתוב במשתנה. צריך להבין גם מהו הטיפוס שלו, מתי השפה בודקת אותו, ואילו כללים מופעלים בזמן הריצה.
הטיפוסים הבסיסיים
בין הטיפוסים הנפוצים נמצאים מספרים שלמים, מספרים עשרוניים, ערכים בוליאניים ומחרוזות.
ב-Java:
int age = 31;
double height = 1.82;
boolean isDeveloper = true;
String name = "Evyatar";ב-TypeScript:
const age: number = 31;
const height: number = 1.82;
const isDeveloper: boolean = true;
const name: string = "Evyatar";ב-Python:
age: int = 31
height: float = 1.82
is_developer: bool = True
name: str = "Evyatar"התחביר דומה, אבל מודל הטיפוסים שונה:
- Java היא שפה סטטית. הטיפוסים נאכפים בזמן הקומפילציה ונשמרים גם במודל הריצה.
- TypeScript בודקת טיפוסים לפני הריצה, אבל הקוד שרץ בפועל הוא JavaScript. רוב מידע הטיפוסים נמחק בתהליך הקומפילציה.
- Python היא שפה דינמית. Type hints מסייעים לעורכים ולבודקי טיפוסים, אך אינם נאכפים אוטומטית בזמן הריצה.
Primitive לעומת Reference ב-Java
ב-Java קיימת הבחנה חשובה בין primitive types לבין reference types.
משתנה primitive מחזיק את הערך עצמו:
int age = 31;
boolean active = true;
char grade = 'A';לעומת זאת, משתנה reference מחזיק הפניה לאובייקט:
String name = new String("Evyatar");במקרה הזה:
- הטיפוס של `name` הוא `String`.
- המשתנה מחזיק reference.
- ה-reference מאפשר להגיע לאובייקט שמייצג את הטקסט `"Evyatar"`.
לא כדאי לחשוב על reference בהכרח ככתובת RAM גלויה. ה-JVM מנהלת את פרטי הזיכרון ויכולה להזיז אובייקטים. מבחינת שפת Java, זהו ערך שמאפשר לזהות אובייקט ולגשת אליו.
ההבחנה משפיעה על העתקה, השוואה ועל האפשרות להשתמש ב-`null`.
`null`, `undefined` ו-`None`
לשלוש השפות יש דרכים שונות לייצג היעדר ערך.
`null` ב-Java
רק משתני reference יכולים להכיל `null`:
String nickname = null;
Integer score = null;טיפוסים פרימיטיביים אינם יכולים להכיל אותו:
int age = null; // שגיאת קומפילציהחשוב גם להבדיל בין שדה למשתנה מקומי. שדה reference שלא אותחל מקבל `null` כברירת מחדל, אבל משתנה מקומי חייב לקבל ערך לפני שאפשר לקרוא ממנו.
`undefined` ו-`null` ב-TypeScript
ב-JavaScript, ולכן גם בזמן הריצה של קוד TypeScript, קיימים שני ערכי היעדר נפוצים:
let city: string | undefined = undefined;
let nickname: string | null = null;בדרך כלל:
- `undefined` מציין שערך לא הוגדר או אינו קיים.
- `null` מציין שהמתכנת בחר במפורש לייצג היעדר ערך.
אחת ההתנהגויות ההיסטוריות והמפתיעות של JavaScript היא:
typeof null // "object"זו אינה הוכחה ש-`null` הוא אובייקט שימושי. זו תאימות לאחור להתנהגות ותיקה של JavaScript.
`None` ב-Python
ב-Python משתמשים ב-`None`:
nickname = Noneהטיפוס של הערך הוא `NoneType`, והדרך המקובלת לבדוק אותו היא באמצעות זהות:
if nickname is None:
print("No nickname")חלוקת מספרים שלמים
אחת המלכודות החשובות ביותר היא חלוקה של מספרים שלמים.
ב-Java, כאשר שני האופרנדים הם `int`, מתבצעת חלוקה שלמה:
int result = 7 / 2; // 3התוצאה נחתכת לכיוון האפס:
7 / 2 // 3
-7 / 2 // -3גם אם שומרים את התוצאה ב-`double`, החלוקה כבר התבצעה:
double result = 7 / 2; // 3.0הביטוי `7 / 2` מחושב לפי טיפוסי האופרנדים. רק לאחר מכן הערך `3` מומר ל-`3.0`.
כדי לקבל חלוקה עשרונית, לפחות אחד האופרנדים צריך להיות עשרוני:
double result = 7 / 2.0; // 3.5ב-TypeScript, הטיפוס `number` מייצג בדרך כלל מספר נקודה צפה בדיוק כפול:
console.log(7 / 2); // 3.5`bigint` הוא טיפוס נפרד, ואי אפשר לערבב אותו ישירות עם `number`.
ב-Python יש שני אופרטורים:
print(7 / 2) # 3.5
print(7 // 2) # 3
print(-7 // 2) # -4`/` מבצע חלוקה רגילה. `//` מבצע floor division, כלומר מעגל כלפי מטה. לכן `-3.5` הופך ל-`-4`, ולא ל-`-3`.
השוואות ב-JavaScript וב-TypeScript
ב-JavaScript קיימים שני אופרטורים נפוצים לשוויון:
"31" == 31 // true
"31" === 31 // false`==` עשוי להמיר טיפוסים לפני ההשוואה. `===` משווה ללא המרה כזאת, ולכן ברוב קוד JavaScript ו-TypeScript מעדיפים אותו.
TypeScript עשויה אפילו להתריע על השוואה בין טיפוסים שאין ביניהם חפיפה. עם זאת, כללי ההשוואה בזמן הריצה הם עדיין הכללים של JavaScript.
`==` ו-`.equals()` ב-Java
ב-Java, המשמעות של `==` תלויה במה שמשווים.
עם primitives הוא משווה ערכים:
int first = 5;
int second = 5;
System.out.println(first == second); // trueעם references הוא בודק אם שני המשתנים מפנים לאותו אובייקט:
String first = new String("Hello");
String second = new String("Hello");
System.out.println(first == second); // false
System.out.println(first.equals(second)); // trueשני האובייקטים מכילים אותו טקסט, אך אינם אותו אובייקט. לכן `==` מחזיר `false`, ואילו `String.equals()` משווה את התוכן.
למה `0.1 + 0.2` אינו בדיוק `0.3`?
Java `double`, JavaScript `number` ו-Python `float` משתמשים בדרך כלל בנקודה צפה בינארית בתקן IEEE 754.
ערכים עשרוניים כמו `0.1` ו-`0.2` אינם ניתנים לייצוג בינארי סופי ומדויק. לכן מתקבלת תוצאה מקורבת:
0.1 + 0.2 = 0.30000000000000004השוואת שוויון מדויקת עלולה להיכשל:
0.1 + 0.2 == 0.3כאשר עובדים עם מדידות מקורבות, אפשר להשוות באמצעות סבילות.
ב-Java:
double a = 0.1 + 0.2;
double b = 0.3;
boolean equal = Math.abs(a - b) < 0.000001;ב-TypeScript:
const a = 0.1 + 0.2;
const b = 0.3;
const equal = Math.abs(a - b) < 0.000001;ב-Python:
a = 0.1 + 0.2
b = 0.3
equal = math.isclose(a, b)הסבילות צריכה להתאים לסולם ולדרישות של התחום. בכסף ובחישובים שבהם ערך עשרוני מדויק הוא חלק מהחוזה, עדיף להשתמש בטיפוסים עשרוניים ייעודיים כגון `BigDecimal` ב-Java או `Decimal` ב-Python.
מה באמת הפתיע אותי?
הדוגמה המפתיעה ביותר היא:
double result = 7 / 2;קל לצפות ל-`3.5` משום שהמשתנה הוא `double`, אבל התוצאה היא `3.0`. הטיפוס של משתנה היעד אינו משנה בדיעבד את הדרך שבה הביטוי כבר חושב.
הפתעה נוספת היא ש-`0.1 + 0.2` אינו שווה בדיוק ל-`0.3`, וששוויון בין אובייקטים ב-Java אינו זהה לשוויון בין הערכים שלהם.
אלה נראים כמו פרטים קטנים, אבל הם משפיעים על תנאים, חישובים, אימות קלט והשוואת נתונים.
סיכום
סוג נתונים אינו רק תווית. הוא משפיע על:
- הערכים שאפשר לשמור;
- הפעולות שמותר לבצע;
- אופן החלוקה וההשוואה;
- ייצוג היעדר ערך;
- הרגע שבו שגיאות מתגלות;
- ואופן הייצוג של מספרים בזיכרון.
Java, TypeScript ו-Python יכולות להציג קוד שנראה דומה, אבל כל אחת מפעילה מודל טיפוסים וכללי ריצה משלה.
כאשר תוצאה נראית מפתיעה, כדאי לבדוק ארבעה דברים: מהם טיפוסי האופרנדים, אילו המרות מתבצעות, האם הבדיקה סטטית או בזמן הריצה, והאם הערך מיוצג בדיוק או בקירוב.