📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is character encoding? Explain ASCII and Unicode, and the difference between a character, a code point and a code unit.

Why text needs an encoding, what ASCII covers and where it stops, how Unicode assigns code points, and how code points, code units, bytes and user-perceived characters (grapheme clusters) differ.

Junior Theory Intermediate ascii computer-basics encoding strings unicode

🎯 Key points

  • An encoding maps characters to numbers and numbers to bytes; bytes alone do not say which encoding was used
  • ASCII is 7-bit: 128 characters (0–127), English letters, digits, punctuation and control codes; nothing else fits
  • "Extended ASCII" code pages such as Windows-1252 and ISO-8859-1 are not ASCII and differ from each other in bytes 128–255
  • Unicode is a character set, not an encoding: it assigns code points U+0000 to U+10FFFF; UTF-8, UTF-16 and UTF-32 are encoding forms that store them
  • A code unit is the storage unit of an encoding (8 bits in UTF-8, 16 bits in UTF-16); one code point can need 1–4 UTF-8 bytes or 1–2 UTF-16 units
  • A user-perceived character (grapheme cluster) can be several code points, and the same text can have composed or decomposed forms, so normalise (usually NFC) before comparing, hashing or using text as a key
  • State the encoding explicitly at every boundary (files, HTTP, databases, queues) and choose a strict decoder for input so bad bytes fail instead of becoming ? or �

⏱ 30-second answer

Computers store numbers, so text needs two agreements: a character set that gives each character a number, and an encoding that turns those numbers into bytes and back. ASCII is both at once: 128 characters (0–127) stored in 7 bits, enough for English but not é, ā or 汉. Unicode is a character set only: it gives every character in every script a code point from U+0000 to U+10FFFF (U+0041 is A, U+00E9 is é). UTF-8, UTF-16 and UTF-32 are the encodings that store code points as code units of 8, 16 or 32 bits. What a user sees as one character, a grapheme cluster, can be several code points (an emoji with a skin-tone modifier, or e plus a combining accent), and the same text can be written in composed or decomposed form, so applications normalise (usually to NFC) before comparing or storing user input.

🗣 Full answer (senior)

Why encodings exist

Memory and files hold bytes. To store text you need an agreed mapping between characters and numbers (a character set) and between those numbers and bytes (an encoding). The same bytes mean different text under different encodings, which is the root of most garbled-text bugs.

ASCII

  • Defined in the 1960s; uses 7 bits, so 128 values (0–127).
  • 0–31 and 127 are control characters (\n is 10, \r is 13, tab is 9); 32–126 are printable (A is 65, a is 97, 0 is 48).
  • Great for English and protocols (HTTP headers, SMTP, JSON syntax), useless for most of the world's languages.
  • So-called extended ASCII used the eighth bit for 128 more characters, but every vendor filled them differently: Windows-1252, ISO-8859-1, code page 437. Byte 0xE9 is é in Windows-1252 but Θ in code page 437.

Unicode: a character set, not an encoding

  • Each character gets a code point written U+XXXX, from U+0000 to U+10FFFF (about 1.1 million values) in 17 planes. Plane 0, the Basic Multilingual Plane (BMP), holds most common characters; emoji and many historic scripts are above U+FFFF.
  • UTF-8, UTF-16 and UTF-32 are the encodings that turn code points into bytes.
  • The first 128 code points are identical to ASCII, and UTF-8 encodes them as the same single bytes, so ASCII text is already valid UTF-8.

Character, code point, code unit, byte

Term Meaning Example: é Example: 👍🏽
Grapheme cluster What the user sees as one character 1 1
Code points Unicode numbers 1 (U+00E9) or 2 (e + U+0301) 2 (U+1F44D + U+1F3FD)
UTF-16 code units (char) 16-bit units, what string.Length counts 1 or 2 4
UTF-8 bytes Bytes on disk or the wire 2 or 3 8

Normalisation of user input

The same visible text can arrive in different forms: a Mac keyboard or file name may send e + combining accent, a Windows form sends precomposed é, and copy-paste from PDFs adds ligatures such as fi. Ordinal comparison treats these as different, so a user cannot log in, a unique index allows "duplicate" names, and a search misses rows. Normalise to NFC (text.Normalize(NormalizationForm.FormC)) at the input boundary, and use NFKC only where compatibility folding is wanted (for example usernames, where fi should equal fi).

Encodings and databases

Store What it holds Watch out for
SQL Server nvarchar UTF-16 n counts code units; safe for all languages
SQL Server varchar with a legacy collation a code page such as 1252 non-Latin characters silently become ?
SQL Server varchar with a _UTF8 collation UTF-8 n counts bytes, so fewer CJK characters fit
PostgreSQL, MySQL utf8mb4, SQLite UTF-8 MySQL's old utf8 holds only 3-byte characters, so emoji fail

If the client, connection and column disagree on encoding, characters are corrupted on the way in, and queries such as WHERE name = @name stop matching because the stored bytes differ from the parameter.

In .NET

  • A string is a sequence of UTF-16 code units; a char is one code unit, not one character.
  • System.Text.Rune is one Unicode scalar value; StringInfo works with grapheme clusters.
  • Encoding.UTF8, Encoding.Unicode (UTF-16LE), Encoding.ASCII and Encoding.Latin1 convert between strings and bytes.

Production example

A customer import used Encoding.ASCII to read uploaded files "because they are CSVs". Every Māori macron (ā) and accented French name became ?, and the data was saved like that. The fix was reading as strict UTF-8, rejecting invalid files with a clear message, normalising names to NFC, and repairing the damaged records from the original uploads.

🔍 What the interviewer is checking

Checks whether the candidate understands text as data, which underpins file handling, APIs and databases.

Green flags: clear separation of character set and encoding, ASCII limits, code points versus code units versus graphemes, knows .NET strings are UTF-16, mentions normalisation and explicit encodings.

Red flags: "Unicode is 16-bit", "a char is a character", treats code pages as ASCII, no idea why text gets garbled.

📚 Learn in depth

1. Layers of text, from screen to bytes

  what the user sees    é        👍🏽
        |
  grapheme clusters     [é]      [👍🏽]
        |
  code points           U+00E9   U+1F44D U+1F3FD
        |  encoding form
  UTF-16 code units     00E9     D83D DC4D D83C DFFD
  UTF-8 bytes           C3 A9    F0 9F 91 8D F0 9F 8F BD

2. One character, two spellings: normalisation

é can be precomposed (U+00E9) or decomposed (e U+0065 + combining acute U+0301). They look identical but compare unequal with ordinal comparison.

  • NFC (composed) is the usual storage form; macOS file names historically used a decomposed form.
  • string.Normalize(NormalizationForm.FormC) converts. Normalise user input before comparing or using it as a key (user names, file names).

3. ASCII control characters you still meet

Code Name Where
0 NUL C strings end with it; it truncates text in some native APIs
9 Tab TSV files
10 LF Line ending on Linux and macOS
13 CR CRLF line endings on Windows and in HTTP
27 ESC ANSI colour codes in terminals

Line endings cause real bugs: a CRLF file read on Linux leaves \r at the end of each line, which breaks parsing and comparisons.

4. Code pages and why they still exist

  • Older systems, mainframe exports and some bank files still use Windows-1252, ISO-8859-1 or EBCDIC.
  • On .NET (Core and later), only UTF-8, UTF-16, UTF-32, ASCII and Latin-1 are built in. For others, call Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) once, then Encoding.GetEncoding(1252).
  • Latin-1 (ISO-8859-1) and Windows-1252 differ in bytes 0x80–0x9F (Windows-1252 has €, curly quotes and dashes there).

5. Comparisons and culture

  • string.Equals(a, b, StringComparison.Ordinal) compares code units: fast and predictable; use it for identifiers, keys and protocol values.
  • Culture-aware comparison treats some different sequences as equal and some letters differently (the Turkish dotless ı problem with ToUpper).
  • Use OrdinalIgnoreCase for case-insensitive identifiers, not ToLower() comparisons.

6. Security angles

  • Homoglyphs: Cyrillic а looks like Latin a; attackers register look-alike user names and domains. Normalise and restrict allowed scripts where identity matters.
  • Invalid sequences: decoders replace invalid bytes with U+FFFD (�) by default; validate input at the boundary instead of silently accepting damage.
  • Overlong encodings were historically used to sneak ../ past filters; modern .NET decoders reject them.

7. Try it yourself

  1. Print "é".Length for the precomposed and decomposed forms ("é"), then compare them with == and after Normalize().
  2. Print "👍🏽".Length, the number of runes (EnumerateRunes().Count()), and new StringInfo("👍🏽").LengthInTextElements.
  3. Encode "Café" with Encoding.ASCII, Encoding.Latin1 and Encoding.UTF8, print the bytes in hex, and decode each with the wrong encoding to see what breaks.

💻 Code

namespace EncodingBasics;

using System.Globalization;
using System.Text;

public static class Program
{
    public static void Main()
    {
        string composed = "é";          // é as one code point
        string decomposed = "é";       // e + combining acute accent

        Console.WriteLine($"{composed.Length} vs {decomposed.Length}");   // 1 vs 2
        Console.WriteLine(composed == decomposed);                          // False
        Console.WriteLine(composed == decomposed.Normalize(NormalizationForm.FormC)); // True

        string thumbs = "\U0001F44D\U0001F3FD";  // thumbs up + medium skin tone
        Console.WriteLine($"chars (UTF-16 code units): {thumbs.Length}");              // 4
        Console.WriteLine($"runes (code points): {thumbs.EnumerateRunes().Count()}");  // 2
        Console.WriteLine($"graphemes: {new StringInfo(thumbs).LengthInTextElements}"); // 1

        foreach (var encoding in new[] { Encoding.ASCII, Encoding.Latin1, Encoding.UTF8 })
        {
            byte[] bytes = encoding.GetBytes("Café");
            Console.WriteLine($"{encoding.WebName,-12} {Convert.ToHexString(bytes)}");
        }
        // us-ascii     4361663F      (é lost, replaced by '?')
        // iso-8859-1   436166E9
        // utf-8        436166C3A9
    }
}

⚠ Common mistakes

  • Mistake: Saying "Unicode is a 16-bit encoding". Why it is wrong: Unicode is a character set with code points up to U+10FFFF (21 bits). UTF-16 is one encoding form of it: code points up to U+FFFF take one 16-bit unit, and the 1,048,576 code points above that take a surrogate pair. UTF-8 uses 1–4 bytes and UTF-32 always 4. Instead: Separate the character set (Unicode) from the encodings (UTF-8, UTF-16, UTF-32).
  • Mistake: Calling Windows-1252 "ASCII". Why it is wrong: ASCII stops at 127; bytes 128–255 mean different things in each code page, so data moves between systems incorrectly. Instead: Name the exact code page, and prefer UTF-8 for anything new.
  • Mistake: Using Encoding.ASCII for user data. Why it is wrong: ASCII can only represent code points 0–127. .NET's Encoding.ASCII uses a replacement fallback by default, so é, ā or 汉 silently become ? with no exception, and the original text is lost for good. Instead: Use UTF-8. Where a legacy partner truly needs ASCII, create the encoding with EncoderFallback.ExceptionFallback so unsupported characters fail loudly, and decide explicitly how to transliterate.
  • Mistake: Decoding untrusted bytes with the default lenient decoder. Why it is wrong: Invalid bytes become U+FFFD (�) and are saved as if valid, so corruption spreads into the database. Instead: Use new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true) at the boundary and reject or quarantine bad files.
  • Mistake: Assuming one char is one character. Why it is wrong: Emoji, many CJK extension characters and combining sequences take several chars, so length checks and substrings cut characters in half. Instead: Use Rune for code points and StringInfo for user-perceived characters.
  • Mistake: Comparing or indexing text without normalising. Why it is wrong: é (U+00E9) and e + U+0301 look identical but are different code points, which breaks log-ins, unique constraints in databases and file lookups (macOS file names historically used decomposed forms). Instead: Normalise to NFC at the boundary (Normalize(NormalizationForm.FormC)) and compare with an explicit StringComparison.
  • Mistake: Relying on a default encoding when exchanging data with legacy systems. Why it is wrong: Defaults differ between APIs, operating systems and tools (Excel, .NET Framework's Encoding.Default code page, mainframe EBCDIC exports). Instead: Pass the encoding explicitly whenever you read or write bytes, and register CodePagesEncodingProvider when you must read a legacy code page.

🏛 Senior insight: trade-offs

Encoding bugs are boundary bugs: they appear wherever text crosses a file, network, database or process boundary and someone assumed the encoding.

Boundary Senior default
Files and queues UTF-8 without BOM, stated in the contract; strict decoding on input
HTTP Content-Type: ...; charset=utf-8 on every text response
SQL Server nvarchar (UTF-16) or a UTF-8 collation with varchar; never a legacy code page for user text
Identity (user names, emails, file names) normalise to NFC, compare ordinally, consider restricting scripts to stop homoglyph spoofing
Legacy partners named code page, documented, converted at one adapter

Seniors validate on input instead of repairing later, because once ? or � is stored the original text cannot be recovered. They also know that user-visible length (grapheme clusters), storage length (bytes) and string.Length (UTF-16 code units) are three different numbers, and pick the right one for each limit.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate names UTF-8 and UTF-16 as Unicode encodings. Why: Goes from concepts to the two encodings .NET developers meet every day.
    How to answer

    Both encode Unicode code points. UTF-8 uses 1–4 bytes per code point and is ASCII-compatible, so it is compact for English (half the memory of UTF-16) and dominates files, HTTP and JSON. UTF-16 uses 2 bytes for the Basic Multilingual Plane and 4 bytes (a surrogate pair) above U+FFFF, such as most emoji; it is more compact than UTF-8 for CJK text (2 bytes instead of 3). .NET strings are UTF-16, and string.Length counts UTF-16 code units, not characters: "👍".Length is 2, "👍🏽".Length is 4, and "é" is 1 if precomposed (U+00E9) but 2 if written as e + combining accent. Count what you mean: EnumerateRunes() for code points, StringInfo for user-visible characters, Encoding.UTF8.GetByteCount for byte limits. On high-traffic paths, stay in UTF-8 bytes (u8 literals, spans, Utf8JsonReader) so you do not transcode and allocate on every request.

  2. When: Candidate says the same bytes mean different text in different encodings. Why: Tests whether the candidate can apply encoding knowledge to a real bug.
    How to answer

    First I look at the bytes, not the screen. José is classic mojibake: the file contains UTF-8 (é is C3 A9) but the reader decoded it as Windows-1252. With Excel on Windows this usually happens when a user double-clicks a CSV that has no BOM: Excel then uses the system's legacy (ANSI) code page, which depends on the Windows locale (1252 for English, 1258 for Vietnamese, and UTF-8 only if the "Use Unicode UTF-8 for worldwide language support" option is turned on). Importing through Data → From Text/CSV lets the user pick UTF-8, but most users double-click. The reliable fix for the export is to write UTF-8 with a BOM (new UTF8Encoding(true)) or produce .xlsx. Jos� is the opposite: a Windows-1252 file (é is the single byte E9) read as UTF-8, which is invalid, so the decoder substitutes U+FFFD. The fix is to read with the right encoding or reject invalid UTF-8 with a clear error. Then I check whether bad data was already saved and repair it separately.

↩ Usually asked after

More in Computer Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me