diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 14d2547b..70e9b94e 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -26,6 +26,7 @@ const AceEditor = lazy(async () => { await import("ace-builds/src-min-noconflict/mode-json"); await import("ace-builds/src-min-noconflict/mode-csv"); await import("ace-builds/src-min-noconflict/mode-text"); + await import("ace-builds/src-min-noconflict/mode-dart"); return ace; } else { throw new Error("should not try SSR"); diff --git a/app/terminal/icons.tsx b/app/terminal/icons.tsx index 872ae930..9c356a0f 100644 --- a/app/terminal/icons.tsx +++ b/app/terminal/icons.tsx @@ -113,6 +113,16 @@ export function LanguageIcon(props: Props) { ); + case "dart": + return ( + + + + ); default: props.lang satisfies never; console.warn("unknown lang for LanguageIcon:", props.lang); diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index ba9a32ec..b40bef1c 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -26,6 +26,7 @@ import sub_h from "./samples/sub.h?raw"; import sub_cpp from "./samples/sub.cpp?raw"; import main2_rs from "./samples/main2.rs?raw"; import sub_rs from "./samples/sub.rs?raw"; +import main_dart from "./samples/main.dart?raw"; import { DaisyInfoIcon } from "@/daisyAlertIcon"; export default function RuntimeTestPage() { @@ -127,6 +128,13 @@ const sampleConfig: Record = { }, exec: ["main2.rs"], }, + dart: { + repl: false, + editor: { + "main.dart": main_dart, + }, + exec: ["main.dart"], + }, }; function RuntimeSample({ lang, diff --git a/app/terminal/samples/main.dart b/app/terminal/samples/main.dart new file mode 100644 index 00000000..a506f97c --- /dev/null +++ b/app/terminal/samples/main.dart @@ -0,0 +1,3 @@ +void main() { + print("Hello, Dart!"); +} diff --git a/packages/runtime/src/context.tsx b/packages/runtime/src/context.tsx index 9874812e..4a7e5ac4 100644 --- a/packages/runtime/src/context.tsx +++ b/packages/runtime/src/context.tsx @@ -3,6 +3,7 @@ import { ReactNode, useEffect } from "react"; import { RuntimeContext } from "./interface"; import { RuntimeLang } from "./languages"; +import { DartProvider, useDart } from "./dart/runtime"; import { TypeScriptProvider, useTypeScript } from "./typescript/runtime"; import { useWandbox, WandboxProvider } from "./wandbox/runtime"; import { JSEvalContext, useJSEval } from "./worker/jsEval"; @@ -34,6 +35,7 @@ export function useRuntimeAll(): Record { const typescript = useTypeScript(jsEval); const wandboxCpp = useWandbox("cpp"); const wandboxRust = useWandbox("rust"); + const dart = useDart(); // initはしない。呼び出し側でする必要がある return { @@ -43,6 +45,7 @@ export function useRuntimeAll(): Record { typescript: typescript, cpp: wandboxCpp, rust: wandboxRust, + dart: dart, }; } export function RuntimeProvider({ children }: { children: ReactNode }) { @@ -51,10 +54,13 @@ export function RuntimeProvider({ children }: { children: ReactNode }) { - {children} + + {children} + ); } + diff --git a/packages/runtime/src/dart/dart-runner.html b/packages/runtime/src/dart/dart-runner.html new file mode 100644 index 00000000..b5d80e01 --- /dev/null +++ b/packages/runtime/src/dart/dart-runner.html @@ -0,0 +1,130 @@ + + + + + + + + + + diff --git a/packages/runtime/src/dart/runtime.tsx b/packages/runtime/src/dart/runtime.tsx new file mode 100644 index 00000000..0408ca5f --- /dev/null +++ b/packages/runtime/src/dart/runtime.tsx @@ -0,0 +1,289 @@ +"use client"; + +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, +} from "react"; +import useSWR from "swr"; +import { + ReplOutput, + RuntimeContext, + RuntimeErrorHandler, + RuntimeInfo, + UpdatedFile, +} from "../interface"; +import dartRunnerHtml from "./dart-runner.html?raw"; + +const DART_PAD_API_BASE = "https://stable.api.dartpad.dev/api/v3"; + +interface DartVersionResponse { + dartVersion?: string; + flutterVersion?: string; +} + +interface AnalysisIssue { + kind: "error" | "warning" | "info"; + message: string; + location: { + charStart: number; + charLength: number; + line: number; + column: number; + }; + code?: string; + correction?: string; + url?: string; +} + +interface AnalysisResponse { + issues: AnalysisIssue[]; + imports?: unknown[]; +} + +const versionFetcher = async (url: string): Promise => { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch Dart version: ${res.statusText}`); + } + return res.json(); +}; + +const DartContext = createContext<{ + init: (onError?: RuntimeErrorHandler) => void; + ready: boolean; + dartVersion?: string; +}>({ + init: () => undefined, + ready: true, +}); + +export function DartProvider({ children }: { children: ReactNode }) { + const onErrorRef = useRef(undefined); + const init = useCallback((onError?: RuntimeErrorHandler) => { + onErrorRef.current = onError; + }, []); + + const { data, error } = useSWR( + `${DART_PAD_API_BASE}/version`, + versionFetcher + ); + + useEffect(() => { + if (error) { + console.error("Failed to fetch Dart version info:", error); + onErrorRef.current?.(error); + } + }, [error]); + + return ( + + {children} + + ); +} + +async function performAnalysis( + source: string, + onOutput: (output: ReplOutput | UpdatedFile) => void +): Promise { + try { + const res = await fetch(`${DART_PAD_API_BASE}/analyze`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ source }), + }); + + if (!res.ok) return; + + const data: AnalysisResponse = await res.json(); + if (Array.isArray(data.issues)) { + for (const issue of data.issues) { + const line = issue.location?.line ?? 1; + const column = issue.location?.column ?? 1; + const kindStr = (issue.kind || "info").toUpperCase(); + const correctionStr = issue.correction ? ` (${issue.correction})` : ""; + const formattedMsg = `[ANALYZER ${kindStr}] line ${line}:${column} - ${issue.message}${correctionStr}`; + + onOutput({ + type: issue.kind === "error" ? "error" : "stderr", + message: formattedMsg, + }); + } + } + } catch (err) { + console.warn("Failed to perform Dart static analysis:", err); + } +} + +export function useDart(): RuntimeContext { + const { init: dartInit, ready, dartVersion } = useContext(DartContext); + const onErrorRef = useRef(undefined); + const activeIframeRef = useRef(null); + + const init = useCallback( + (onError?: RuntimeErrorHandler) => { + onErrorRef.current = onError; + dartInit(onError); + }, + [dartInit] + ); + + const interrupt = useCallback(() => { + if (activeIframeRef.current) { + if (activeIframeRef.current.parentNode) { + activeIframeRef.current.parentNode.removeChild(activeIframeRef.current); + } + activeIframeRef.current = null; + } + }, []); + + const runFiles = useCallback( + async ( + filenames: string[], + files: Readonly>, + onOutput: (output: ReplOutput | UpdatedFile) => void + ) => { + if (typeof window === "undefined") { + onOutput({ + type: "error", + message: "Dart runtime requires browser environment.", + }); + return; + } + + const filename = filenames[0] ?? Object.keys(files)[0]; + const source = files[filename] ?? Object.values(files)[0]; + + if (!source) { + onOutput({ type: "error", message: "No source code provided to run." }); + return; + } + + try { + const [, response] = await Promise.all([ + performAnalysis(source, onOutput), + fetch(`${DART_PAD_API_BASE}/compileNewDDC`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ source }), + }), + ]); + + if (!response.ok) { + const errorText = await response.text(); + onOutput({ + type: "error", + message: + errorText || `Compilation failed with status ${response.status}`, + }); + return; + } + + const data = await response.json(); + if (!data.result) { + onOutput({ + type: "error", + message: "Compilation returned empty result.", + }); + return; + } + + const jsCode: string = data.result; + + // Execute compiled JS inside a temporary iframe + await new Promise((resolve) => { + const iframe = document.createElement("iframe"); + iframe.style.display = "none"; + iframe.srcdoc = dartRunnerHtml; + document.body.appendChild(iframe); + activeIframeRef.current = iframe; + + let resolved = false; + const cleanup = () => { + if (resolved) return; + resolved = true; + window.removeEventListener("message", handleMessage); + if (activeIframeRef.current === iframe) { + activeIframeRef.current = null; + } + if (iframe.parentNode) { + iframe.parentNode.removeChild(iframe); + } + resolve(); + }; + + const handleMessage = (event: MessageEvent) => { + if (event.source !== iframe.contentWindow) return; + const msgData = event.data; + if (!msgData || msgData.sender !== "dart_frame") return; + + if (msgData.type === "stdout") { + onOutput({ type: "stdout", message: String(msgData.message) }); + } else if (msgData.type === "done") { + cleanup(); + } else if (msgData.type === "jserr") { + onOutput({ type: "error", message: String(msgData.message) }); + cleanup(); + } + }; + + window.addEventListener("message", handleMessage); + + // iframeが読み込まれたら、コンパイル済みのコードを送信して実行させる + iframe.onload = () => { + iframe.contentWindow?.postMessage( + { + type: "EXECUTE_DART", + code: jsCode, + }, + "*" + ); + }; + }); + } catch (error) { + onErrorRef.current?.(error); + onOutput({ + type: "fatalError", + message: error instanceof Error ? error.message : String(error), + }); + } + }, + [] + ); + + const runtimeInfo = useMemo( + () => ({ + prettyLangName: "Dart", + version: dartVersion, + }), + [dartVersion] + ); + + return { + init, + ready, + runFiles, + interrupt, + getCommandlineStr, + runtimeInfo, + }; +} + +function getCommandlineStr(filenames: string[]) { + return `dart run ${filenames[0] ?? "main.dart"}`; +} diff --git a/packages/runtime/src/languages.ts b/packages/runtime/src/languages.ts index 3c2ceb72..2b4ff665 100644 --- a/packages/runtime/src/languages.ts +++ b/packages/runtime/src/languages.ts @@ -22,7 +22,8 @@ export type MarkdownLang = | "makefile" | "cmake" | "text" - | "txt"; + | "txt" + | "dart"; export type RuntimeLang = | "python" @@ -30,7 +31,8 @@ export type RuntimeLang = | "cpp" | "rust" | "javascript" - | "typescript"; + | "typescript" + | "dart"; export type LangConstants = { originalLang: MarkdownLang | undefined; @@ -50,7 +52,8 @@ export type LangConstants = { | "json" | "ini" | "makefile" - | "cmake"; + | "cmake" + | "dart"; } & ( | { // terminal/editor.tsx でimportする mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値と対応する @@ -63,7 +66,8 @@ export type LangConstants = { | "typescript" | "json" | "csv" - | "text"; + | "text" + | "dart"; tabSize: number; } | { @@ -158,6 +162,14 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { tabSize: 4, runtime: "rust", }; + case "dart": + return { + originalLang: lang, + rsh: "dart", + ace: "dart", + tabSize: 2, + runtime: "dart", + }; case "bash": case "sh": return { originalLang: lang, rsh: "bash" }; @@ -198,3 +210,4 @@ export function langConstants(lang: MarkdownLang | undefined): LangConstants { return { originalLang: lang }; } } + diff --git a/packages/runtime/tests/fileExecution.ts b/packages/runtime/tests/fileExecution.ts index ee1b9617..88542853 100644 --- a/packages/runtime/tests/fileExecution.ts +++ b/packages/runtime/tests/fileExecution.ts @@ -20,6 +20,7 @@ export const fileExecutionTests: Record< rust: ["test.rs", `fn main() {\n println!("${msg}");\n}\n`], javascript: ["test.js", `console.log("${msg}")`], typescript: ["test.ts", `console.log("${msg}")`], + dart: ["main.dart", `void main() {\n print("${msg}");\n}\n`], } satisfies Record )[lang]; if (!filename || !code) return null; @@ -53,6 +54,10 @@ export const fileExecutionTests: Record< rust: ["test_error.rs", `fn main() {\n panic!("${errorMsg}");\n}\n`], javascript: ["test_error.js", `throw new Error("${errorMsg}");\n`], typescript: ["test_error.ts", `throw new Error("${errorMsg}");\n`], + dart: [ + "test_error.dart", + `void main() {\n throw Exception("${errorMsg}");\n}\n`, + ], } satisfies Record )[lang]; if (!filename || !code) return null; @@ -114,6 +119,7 @@ export const fileExecutionTests: Record< ], javascript: [null, null], typescript: [null, null], + dart: [null, null], } satisfies Record< RuntimeLang, [Record, string[]] | [null, null] @@ -148,6 +154,7 @@ export const fileExecutionTests: Record< rust: [null, null], javascript: [null, null], typescript: [null, null], + dart: [null, null], } satisfies Record )[lang]; if (!filename || !code) return null; diff --git a/packages/runtime/tests/repl.ts b/packages/runtime/tests/repl.ts index e47d5da3..999ff2b4 100644 --- a/packages/runtime/tests/repl.ts +++ b/packages/runtime/tests/repl.ts @@ -19,6 +19,7 @@ export const replTests: Record TestBody | null> = rust: null, javascript: `console.log("${msg}")`, typescript: null, + dart: null, } satisfies Record )[lang]; if (!printCode) return null; @@ -49,6 +50,7 @@ export const replTests: Record TestBody | null> = `console.log(${varName})`, ], typescript: [null, null], + dart: [null, null], } satisfies Record )[lang]; if (!setIntVarCode || !printIntVarCode) return null; @@ -87,6 +89,7 @@ export const replTests: Record TestBody | null> = rust: null, javascript: `throw new Error("${errorMsg}")`, typescript: null, + dart: null, } satisfies Record )[lang]; if (!errorCode) return null; @@ -118,6 +121,7 @@ export const replTests: Record TestBody | null> = `console.log(testVar)`, ], typescript: [null, null, null], + dart: [null, null, null], } satisfies Record )[lang]; if (!setIntVarCode || !infLoopCode || !printIntVarCode) return null; @@ -168,6 +172,7 @@ export const replTests: Record TestBody | null> = rust: null, javascript: null, typescript: null, + dart: null, } satisfies Record )[lang]; if (!writeCode) return null; diff --git a/packages/runtime/tests/utils.ts b/packages/runtime/tests/utils.ts index f9878cb0..15c98d29 100644 --- a/packages/runtime/tests/utils.ts +++ b/packages/runtime/tests/utils.ts @@ -9,6 +9,7 @@ export const RUNTIME_TIMEOUTS: Record = { typescript: 2000, cpp: 10000, rust: 20000, + dart: 15000, }; export async function waitForRuntimeReady( diff --git a/public/docs/dart/0-intro/-intro.md b/public/docs/dart/0-intro/-intro.md new file mode 100644 index 00000000..4349a8a7 --- /dev/null +++ b/public/docs/dart/0-intro/-intro.md @@ -0,0 +1,5 @@ +他のプログラミング言語の経験がある皆さん、[[Dart]]の世界へようこそ。 + +DartはGoogleによって開発された、クライアントサイド(モバイル・Web・デスクトップ)およびサーバーサイドで幅広く活用されているモダンなオブジェクト指向言語です。特にクロスプラットフォームフレームワークである[[Flutter]]の開発言語として急速に普及しました。 + +この章では、Dartがどのような言語思想で作られ、どのような特徴を持っているかを概観し、Dartプログラムのエントリーポイントとなる [[`main`関数]] の書き方を学びます。 diff --git a/public/docs/dart/0-intro/1-0-features.md b/public/docs/dart/0-intro/1-0-features.md new file mode 100644 index 00000000..7ff96a23 --- /dev/null +++ b/public/docs/dart/0-intro/1-0-features.md @@ -0,0 +1,16 @@ +--- +id: dart-intro-features +title: Dartの特徴(JIT/AOTコンパイル、UI最適化言語) +level: 2 +question: + - なぜDartはクライアント開発と相性が良いのですか? + - JavaScriptやTypeScriptと比べたDartの強みは何ですか? +term: + - Dart +--- + +## Dartの特徴(JIT/AOTコンパイル、UI最適化言語) + +[[Dart]]は、美しく高速なユーザーインターフェース(UI)を構築するために設計された、オブジェクト指向の型安全なプログラミング言語です。 + +クライアントサイド(モバイル、Web、デスクトップ)の開発体験を極限まで高めるため、実行基盤や言語構文に独自の工夫が施されています。 diff --git a/public/docs/dart/0-intro/1-1-jit-aot.md b/public/docs/dart/0-intro/1-1-jit-aot.md new file mode 100644 index 00000000..5cdcca95 --- /dev/null +++ b/public/docs/dart/0-intro/1-1-jit-aot.md @@ -0,0 +1,21 @@ +--- +id: dart-intro-jit-aot +title: JITとAOTのハイブリッドな実行基盤 +level: 3 +question: + - JITコンパイルとAOTコンパイルの両方をサポートしているメリットは何ですか? + - ホットリロードはどのような仕組みで動いていますか? +term: + - JITコンパイル + - AOTコンパイル + - JIT + - AOT + - ホットリロード +--- + +### JITとAOTのハイブリッドな実行基盤 + +Dart最大の特徴の1つは、**[[JITコンパイル]](Just-In-Time)** と **[[AOTコンパイル]](Ahead-Of-Time)** の両方に対応している点です。 + +* **開発時(JIT)**: ソースコードを実行時に即座に解釈・コンパイルします。これにより、コード変更を瞬時にアプリへ反映する **[[ホットリロード]](Hot Reload)** が可能になり、開発サイクルが劇的に高速化します。 +* **リリース時(AOT)**: 各プラットフォーム(ARMやx86のネイティブ機械語、あるいは最適化されたJavaScript/WebAssembly)へ事前にコンパイルします。起動の速さとなめらかな描画フレームレート(60fps / 120fps)を実現します。 diff --git a/public/docs/dart/0-intro/1-2-ui-optimized.md b/public/docs/dart/0-intro/1-2-ui-optimized.md new file mode 100644 index 00000000..acbd9579 --- /dev/null +++ b/public/docs/dart/0-intro/1-2-ui-optimized.md @@ -0,0 +1,18 @@ +--- +id: dart-intro-ui-optimized +title: UI構築に最適化された言語機能 +level: 3 +question: + - なぜDartはUI開発に最適化された言語と言われるのですか? + - オブジェクト生成時のnew省略はどのようにUIコードをすっきりさせますか? +term: + - UI最適化 +--- + +### UI構築に最適化された言語機能 + +Dartはユーザーインターフェースのツリー構造を宣言的に記述しやすくするため、言語仕様レベルで多くの工夫が施されています。 + +* **オブジェクト生成時の `new` 省略**: ネストしたUIコンポーネントをスッキリ記述できます。 +* **コレクション要素内の制御構文**: 配列やリストの内部で直接 `if` や `for`、スプレッド演算子(`...`)が使えます([[./4]]で学習)。 +* **健全な型システムと[[Null Safety]]**: 実行時クラッシュの原因となりやすいNull関連エラーをコンパイル時に検知します([[./2]]で学習)。 diff --git a/public/docs/dart/0-intro/2-0-flutter.md b/public/docs/dart/0-intro/2-0-flutter.md new file mode 100644 index 00000000..c553b232 --- /dev/null +++ b/public/docs/dart/0-intro/2-0-flutter.md @@ -0,0 +1,30 @@ +--- +id: dart-intro-flutter +title: DartとFlutter +level: 2 +question: + - DartとFlutterの関係はどうなっていますか? + - Flutter以外の場所でもDartは使えますか? +term: + - Flutter + - フラッター + - マルチプラットフォーム +--- + +## DartとFlutter + +[[Dart]]の存在を語る上で欠かせないのが、UIフレームワークである **[[Flutter]]** です。 + +Flutterは、単一のコードベースからiOS、Android、Web、macOS、Windows、Linux向けの高性能なアプリケーションをビルドできる[[マルチプラットフォーム]]フレームワークです。 + +``` ++-------------------------------------------------------------+ +| Flutter Framework (Dart製UIライブラリ) | +| (Widgets, Rendering, Animation, Material, Cupertino) | ++-------------------------------------------------------------+ +| Dart Platform (言語・ランタイム・標準ライブラリ) | +| (Core, Async, Collections, Math, I/O, Isolates) | ++-------------------------------------------------------------+ +| Flutter Engine (C++ / Skia・Impeller) | ++-------------------------------------------------------------+ +``` diff --git a/public/docs/dart/0-intro/2-1-why-flutter.md b/public/docs/dart/0-intro/2-1-why-flutter.md new file mode 100644 index 00000000..859a3858 --- /dev/null +++ b/public/docs/dart/0-intro/2-1-why-flutter.md @@ -0,0 +1,19 @@ +--- +id: dart-intro-why-flutter +title: DartがFlutterに選ばれた理由 +level: 3 +question: + - なぜFlutterはJavaScriptやC++ではなくDartを選んだのですか? + - Dart単体でサーバーサイドアプリケーションを書くことはできますか? +term: + - 宣言的UI +--- + +### DartがFlutterに選ばれた理由 + +1. **高速な開発体験**: JITによるミリ秒単位のステートフル・ホットリロード。 +2. **高い実行時パフォーマンス**: AOTコンパイルによるネイティブバイナリの生成と、UIの頻繁なオブジェクト生成・破棄に特化した世代別ガベージコレクション。 +3. **[[宣言的UI]]との親和性**: 特別なマークアップ言語(XMLやJSXなど)を必要とせず、Dart言語そのものでUIツリーをきれいに記述可能。 + +> [!NOTE] +> DartはFlutter専用言語ではありません。`dart:io` や `shelf` などのライブラリを用いてCLIツールやバックエンドAPIサーバーの開発にも活用されています。 diff --git a/public/docs/dart/0-intro/3-0-install.md b/public/docs/dart/0-intro/3-0-install.md new file mode 100644 index 00000000..6ae506b7 --- /dev/null +++ b/public/docs/dart/0-intro/3-0-install.md @@ -0,0 +1,20 @@ +--- +id: dart-intro-install +title: Dartのインストール +level: 2 +question: + - FlutterをインストールすればDart SDKも一緒に含まれますか? + - pubspec.yaml とは何をするファイルですか? +term: + - Dart SDK + - dart コマンド + - pub + - pubspec.yaml +--- + +## Dartのインストール + +ローカル環境で[[Dart]]を動作させるには、**[[Dart SDK]]** をインストールします。 + +> [!TIP] +> 既に [[Flutter]] をインストールしている場合、Flutter SDKにDart SDKがバンドルされているため、個別のDartインストールは不要です。 diff --git a/public/docs/dart/0-intro/3-1-install-methods.md b/public/docs/dart/0-intro/3-1-install-methods.md new file mode 100644 index 00000000..94f5a0a3 --- /dev/null +++ b/public/docs/dart/0-intro/3-1-install-methods.md @@ -0,0 +1,36 @@ +--- +id: dart-intro-install-methods +title: インストール方法 +level: 3 +question: + - macOSやWindowsでのDartのインストールコマンドは何ですか? + - インストールが成功したか確認する方法を教えてください。 +term: + - Homebrew + - Chocolatey +--- + +### インストール方法 + +主要なOSでのインストール手順は以下の通りです。 + +* **macOS (Homebrew)**: + ```bash + brew tap dart-lang/dart + brew install dart + ``` +* **Windows (Winget / Chocolatey)**: + ```bash + winget install Dart.Dart-SDK + ``` +* **Linux (apt)**: + ```bash + sudo apt-get install dart + ``` + +ターミナルで `dart` コマンドを実行し、バージョンが表示されれば完了です。 + +```bash +$ dart --version +Dart SDK version: 3.x.x +``` diff --git a/public/docs/dart/0-intro/3-2-create-run.md b/public/docs/dart/0-intro/3-2-create-run.md new file mode 100644 index 00000000..9a35fac3 --- /dev/null +++ b/public/docs/dart/0-intro/3-2-create-run.md @@ -0,0 +1,29 @@ +--- +id: dart-intro-create-run +title: プロジェクトの作成と実行 +level: 3 +question: + - dart create コマンドで新規プロジェクトを作成する方法は? + - プロジェクトの設定やライブラリ追加はどのファイルで行いますか? +term: + - dart create + - dart run +--- + +### プロジェクトの作成と実行 + +Dartでは、プロジェクト作成や依存関係管理、コード整形、テストなどのツールチェーンがすべて `dart` コマンドに統合されています。 + +```bash +# 新しいコンソールプロジェクトを作成 +$ dart create my_dart_app + +# プロジェクトディレクトリへ移動 +$ cd my_dart_app + +# プログラムの実行 +$ dart run +Hello world: 42! +``` + +Dartのプロジェクト構成やライブラリの依存管理は、プロジェクトルートにある `pubspec.yaml` ファイルで行います。 diff --git a/public/docs/dart/0-intro/4-0-main.md b/public/docs/dart/0-intro/4-0-main.md new file mode 100644 index 00000000..a8071a08 --- /dev/null +++ b/public/docs/dart/0-intro/4-0-main.md @@ -0,0 +1,32 @@ +--- +id: dart-intro-main +title: 'エントリーポイント: main() 関数' +level: 2 +question: + - main関数の戻り値型はvoid以外も使えますか? + - print関数で出力した文字列の末尾には自動的に改行が入りますか? +term: + - main関数 + - main() + - print + - print関数 +--- + +## エントリーポイント: `main()` 関数 + +C言語、Java、Rustなどと同様に、[[Dart]]プログラムはトップレベルの **[[main関数]](`main()`)** から実行が始まります。 + +```dart:hello_world.dart +void main() { + print('Hello, Dart!'); +} +``` + +```dart-exec:hello_world.dart +Hello, Dart! +``` + +* `void`: `main` 関数が値を返さないことを表します。 +* `main()`: アプリケーションのエントリーポイントとなる関数名です。 +* `print(...)`: 文字列などのオブジェクトを標準出力に出力し、末尾に改行を追加します。 +* `;` (セミコロン): Dartでは各ステートメントの末尾にセミコロンが必須です。 diff --git a/public/docs/dart/0-intro/4-1-args.md b/public/docs/dart/0-intro/4-1-args.md new file mode 100644 index 00000000..252d1cb3 --- /dev/null +++ b/public/docs/dart/0-intro/4-1-args.md @@ -0,0 +1,31 @@ +--- +id: dart-intro-args +title: コマンドライン引数の受け取り +level: 3 +question: + - コマンドライン引数をmain関数で受け取るにはどうすればよいですか? + - argsの型は何になりますか? +term: + - コマンドライン引数 + - args +--- + +### コマンドライン引数の受け取り + +外部からの引数を受け取る場合は、引数に `List args` を指定します。 + +```dart:args_example.dart +void main(List args) { + if (args.isEmpty) { + print('引数が渡されていません。'); + } else { + print('受け取った引数: $args'); + } +} +``` + +```dart-exec:args_example.dart +引数が渡されていません。 +``` + +次の [[./1]] では、Dartの型システム、変数宣言、および基本的な演算子について詳しく学んでいきます。 diff --git a/public/docs/dart/1-basics/-intro.md b/public/docs/dart/1-basics/-intro.md new file mode 100644 index 00000000..5131fee1 --- /dev/null +++ b/public/docs/dart/1-basics/-intro.md @@ -0,0 +1,4 @@ +この章では、[[Dart]]の基本的な文法と型システムについて学びます。 + +Dartは静的型付け言語ですが、優れた[[型推論]]を備えており、冗長な型記述を避けつつ型安全性を保つことができます。 +また、他の言語とは一味違う `var`, `dynamic`, `Object`, `final`, `const` などのキーワードの使い分けを理解することで、堅牢で読みやすいコードの土台を築きます。 diff --git a/public/docs/dart/1-basics/1-0-variables.md b/public/docs/dart/1-basics/1-0-variables.md new file mode 100644 index 00000000..d175b723 --- /dev/null +++ b/public/docs/dart/1-basics/1-0-variables.md @@ -0,0 +1,18 @@ +--- +id: dart-basics-variables +title: 変数宣言と型推論 +level: 2 +question: + - 型を明示する場合とvarを使う場合の使い分けの目安はありますか? + - 初期化せずに変数宣言したときの初期値は何ですか? +term: + - 変数 + - 変数宣言 + - 型推論 +--- + +## 変数宣言と型推論 + +[[Dart]]は静的型付け言語ですが、初期値から型を自動で推論する **[[型推論]]** を強力にサポートしています。 + +Dartでの変数宣言は、`var` による型推論と、型を明示する宣言の2通りが基本となります。 diff --git a/public/docs/dart/1-basics/1-1-var-type-inference.md b/public/docs/dart/1-basics/1-1-var-type-inference.md new file mode 100644 index 00000000..59ef7381 --- /dev/null +++ b/public/docs/dart/1-basics/1-1-var-type-inference.md @@ -0,0 +1,44 @@ +--- +id: dart-basics-type-inference +title: var による型推論と型明示 +level: 3 +question: + - varで宣言した変数に異なる型の値を再代入できますか? + - 型を明示するべき場面はどのようなときですか? +term: + - var + - 静的型付け +--- + +### `var` による型推論と型明示 + +初期値が存在する場合、`var` キーワードを使うとコンパイラが自動的に型を決定します。 + +```dart:variables_intro.dart +void main() { + var name = 'Dart'; // String 型と推論される + var version = 3; // int 型と推論される + + print('$name のバージョン: $version'); +} +``` + +```dart-exec:variables_intro.dart +Dart のバージョン: 3 +``` + +一度型が推論された変数に異なる型の値を代入しようとすると、コンパイルエラーになります。 + +型を明示的に記述することも可能です。 + +```dart:type_explicit.dart +void main() { + String message = 'Hello'; + int count = 10; + print('$message: $count 件'); +} +``` + +```dart-exec:type_explicit.dart +Hello: 10 件 +``` diff --git a/public/docs/dart/1-basics/1-2-var-dynamic-object.md b/public/docs/dart/1-basics/1-2-var-dynamic-object.md new file mode 100644 index 00000000..5a8b48f9 --- /dev/null +++ b/public/docs/dart/1-basics/1-2-var-dynamic-object.md @@ -0,0 +1,48 @@ +--- +id: dart-basics-var-dynamic-object +title: 'var、dynamic、Object の違い' +level: 3 +question: + - dynamicとObjectの違いは何ですか? + - dynamic型を使うべきシチュエーションはどのようなときですか? +term: + - dynamic + - Object + - 動的型 +--- + +### `var`、`dynamic`、`Object` の違い + +Dartには一見似ているように思える `var`, `dynamic`, `Object` というキーワードが存在します。これらは型安全性において決定的な違いがあります。 + +| キーワード | 静的型チェック | 別の型の再代入 | メンバーアクセス | +| :--- | :--- | :--- | :--- | +| **`var` (初期化あり)** | あり (初期値から固定) | 不可 | 推論された型のメソッドのみ | +| **`dynamic`** | なし (実行時に解決) | 可能 | なんでも呼べる (存在しなければ実行時エラー) | +| **`Object` / `Object?`** | あり (すべての型の基底) | 可能 | `Object` が持つメソッド (`toString()` 等) のみ | + +```dart:dynamic_vs_object.dart +void main() { + // 1. dynamic: 静的型チェックを完全にバイパスする + dynamic value = 'Hello'; + print('dynamic: ${value.length}'); + value = 123; // 異なる型の再代入もOK + print('dynamic(int): $value'); + + // 2. Object: すべての非Nullオブジェクトの基底クラス (型安全) + Object obj = 'World'; + if (obj is String) { + // is チェックでスマートキャストされる + print('Object(String): ${obj.length}'); + } +} +``` + +```dart-exec:dynamic_vs_object.dart +dynamic: 5 +dynamic(int): 123 +Object(String): 5 +``` + +> [!WARNING] +> `dynamic` は実行時までエラーが発覚しないため、JSONのデコードなど型が未知の境界領域以外では極力使用を避けましょう。 diff --git a/public/docs/dart/1-basics/1-3-final-const.md b/public/docs/dart/1-basics/1-3-final-const.md new file mode 100644 index 00000000..876cced5 --- /dev/null +++ b/public/docs/dart/1-basics/1-3-final-const.md @@ -0,0 +1,43 @@ +--- +id: dart-basics-final-const +title: final と const の使い分け +level: 3 +question: + - finalとconstの最も重要な違いは何ですか? + - DateTime.now()をconstに代入できないのはなぜですか? +term: + - final + - const + - コンパイル時定数 + - 不変 +--- + +### `final` と `const` の使い分け + +Dartで値の再代入を禁止する変数(定数)を宣言するには、`final` または `const` を使用します。 + +* **`final`(実行時定数)**: 実行時に一度だけ初期化され、以降は変更できません。`DateTime.now()` などの実行時計算値も代入可能です。 +* **`const`(コンパイル時定数)**: コンパイル時に値が完全に確定している定数です。 + +```dart:final_const.dart +void main() { + final now = DateTime.now(); // 実行時に値が確定 + const maxItems = 100; // コンパイル時に確定 + + // constリストは要素の変更も不可(完全なイミュータブル) + const list = [1, 2, 3]; + + print('現在時刻 (final): $now'); + print('最大件数 (const): $maxItems'); + print('定数リスト: $list'); +} +``` + +```dart-exec:final_const.dart +現在時刻 (final): 2026-08-14 05:20:00.000 +最大件数 (const): 100 +定数リスト: [1, 2, 3] +``` + +> [!TIP] +> Flutterでは不変なWidgetの生成に `const` を付与することで、再描画時のインスタンス再生成を防止してパフォーマンスを高めます。 diff --git a/public/docs/dart/1-basics/2-0-builtin-types.md b/public/docs/dart/1-basics/2-0-builtin-types.md new file mode 100644 index 00000000..b39f055f --- /dev/null +++ b/public/docs/dart/1-basics/2-0-builtin-types.md @@ -0,0 +1,21 @@ +--- +id: dart-basics-builtin-types +title: 組み込み型と文字列操作 +level: 2 +question: + - int型とdouble型の共通の親クラスは何ですか? + - Dartの組み込み型はすべてオブジェクトですか? +term: + - 組み込み型 + - int + - double + - num + - String + - bool +--- + +## 組み込み型と文字列操作 + +[[Dart]]のすべての値はオブジェクトであり、数値や真偽値も含めて `Object` を継承しています。 + +主要な組み込み型には、整数 `int`、浮動小数点数 `double`、その共通親型 `num`、文字列 `String`、真偽値 `bool` があります。 diff --git a/public/docs/dart/1-basics/2-1-numbers.md b/public/docs/dart/1-basics/2-1-numbers.md new file mode 100644 index 00000000..82f580c8 --- /dev/null +++ b/public/docs/dart/1-basics/2-1-numbers.md @@ -0,0 +1,41 @@ +--- +id: dart-basics-numbers +title: 数値と型変換 +level: 3 +question: + - 文字列からintやdoubleにパースする方法は? + - doubleを小数点第1位までに丸めて文字列にする方法は? +term: + - 型変換 + - int.parse + - double.parse +--- + +### 数値と型変換 + +`int` と `double` は相互に変換でき、文字列との相互変換もメソッドが用意されています。 + +```dart:numbers.dart +void main() { + int integer = 42; + double decimal = 3.14; + num both = 10; + both = 2.5; // num型ならdoubleも代入可能 + + // 文字列から数値への変換 + int parsedInt = int.parse('100'); + double parsedDouble = double.parse('12.34'); + + // 数値から文字列への変換 + String strInt = integer.toString(); + String fixedDec = decimal.toStringAsFixed(1); // "3.1" + + print('parsed: $parsedInt, $parsedDouble'); + print('converted: $strInt, $fixedDec'); +} +``` + +```dart-exec:numbers.dart +parsed: 100, 12.34 +converted: 42, 3.1 +``` diff --git a/public/docs/dart/1-basics/2-2-strings.md b/public/docs/dart/1-basics/2-2-strings.md new file mode 100644 index 00000000..f3341eff --- /dev/null +++ b/public/docs/dart/1-basics/2-2-strings.md @@ -0,0 +1,44 @@ +--- +id: dart-basics-strings +title: 文字列と文字列補間 +level: 3 +question: + - Dartで文字列補間(String Interpolation)はどう書きますか? + - 複数行文字列(トリプルクォート)の使い方は? +term: + - 文字列補間 + - 複数行文字列 +--- + +### 文字列と文字列補間 + +Dartでは、文字列リテラル内に `$変数名` や `${式}` を埋め込むことができます。シングルクォート `'` とダブルクォート `"` のどちらでも記述可能です。 + +```dart:strings.dart +void main() { + String language = 'Dart'; + int version = 3; + + // 文字列補間 + String greeting = 'Welcome to $language $version!'; + String calc = '1 + 1 = ${1 + 1}'; + + // 複数行文字列(トリプルクォート) + String multiLine = ''' +1行目のテキスト +2行目のテキスト +3行目のテキスト'''; + + print(greeting); + print(calc); + print(multiLine); +} +``` + +```dart-exec:strings.dart +Welcome to Dart 3! +1 + 1 = 2 +1行目のテキスト +2行目のテキスト +3行目のテキスト +``` diff --git a/public/docs/dart/1-basics/3-0-operators.md b/public/docs/dart/1-basics/3-0-operators.md new file mode 100644 index 00000000..b5b2bbdc --- /dev/null +++ b/public/docs/dart/1-basics/3-0-operators.md @@ -0,0 +1,17 @@ +--- +id: dart-basics-operators +title: 基本的な演算子 +level: 2 +question: + - Dart特有の便利な演算子には何がありますか? + - 整数除算演算子の記号は何ですか? +term: + - 演算子 + - 算術演算子 + - 比較演算子 + - 論理演算子 +--- + +## 基本的な演算子 + +Dartには一般的な言語と同様の算術・比較・論理演算子に加えて、整数除算や型判定などDart特有の便利な演算子が用意されています。 diff --git a/public/docs/dart/1-basics/3-1-arithmetic.md b/public/docs/dart/1-basics/3-1-arithmetic.md new file mode 100644 index 00000000..9dc7f919 --- /dev/null +++ b/public/docs/dart/1-basics/3-1-arithmetic.md @@ -0,0 +1,35 @@ +--- +id: dart-basics-arithmetic +title: 算術演算子と整数除算(~/) +level: 3 +question: + - 通常の除算 / と整数除算 ~/ の違いは何ですか? + - 剰余演算子 % の使い方は? +term: + - 整数除算 + - '~/' + - 剰余 +--- + +### 算術演算子と整数除算(`~/`) + +通常の除算 `/` は常に `double` を返します。商の整数部分のみを取得したい場合は **`~/`**(整数除算演算子)を使います。 + +```dart:operators_arithmetic.dart +void main() { + int a = 10; + int b = 3; + + print('加算 (+): ${a + b}'); + print('通常除算 (/): ${a / b}'); // double (3.3333333333333335) + print('整数除算 (~/): ${a ~/ b}'); // int (3) + print('剰余 (%): ${a % b}'); // int (1) +} +``` + +```dart-exec:operators_arithmetic.dart +加算 (+): 13 +通常除算 (/): 3.3333333333333335 +整数除算 (~/): 3 +剰余 (%): 1 +``` diff --git a/public/docs/dart/1-basics/3-2-type-test.md b/public/docs/dart/1-basics/3-2-type-test.md new file mode 100644 index 00000000..583a659d --- /dev/null +++ b/public/docs/dart/1-basics/3-2-type-test.md @@ -0,0 +1,42 @@ +--- +id: dart-basics-type-test +title: 型テスト演算子と三項条件演算子 +level: 3 +question: + - is や is! 演算子は何のために使われますか? + - 型テストを行った後に自動でキャストされる仕組み(スマートキャスト)とは? +term: + - 型テスト演算子 + - 'is' + - 'is!' + - 三項演算子 + - スマートキャスト +--- + +### 型テスト演算子と三項条件演算子 + +オブジェクトが特定の型であるかを判定するには、`is` や `is!` を使います。 + +* `is`: 指定した型であれば `true`(スコープ内で自動的にスマートキャストされる) +* `is!`: 指定した型でなければ `true` + +```dart:type_test.dart +void main() { + Object value = 'Dart Programming'; + + if (value is String) { + // ifスコープ内では value が String にスマートキャストされる + print('文字列の長さ: ${value.length}'); + } + + // 三項条件演算子 + int score = 85; + String result = score >= 60 ? '合格' : '不合格'; + print('判定: $result'); +} +``` + +```dart-exec:type_test.dart +文字列の長さ: 16 +判定: 合格 +``` diff --git a/public/docs/dart/1-basics/4-0-summary.md b/public/docs/dart/1-basics/4-0-summary.md new file mode 100644 index 00000000..3109410a --- /dev/null +++ b/public/docs/dart/1-basics/4-0-summary.md @@ -0,0 +1,18 @@ +--- +id: dart-basics-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]の基本構文と型システムの基本を学びました。 + +* **型推論と変数宣言**: `var` を使うと初期値から静的型が自動推論される。型を明示することも可能。 +* **`dynamic` と `Object`**: `dynamic` は型チェックを無効化し、`Object` は全オブジェクトの基底型として安全に扱える。 +* **`final` と `const`**: `final` は実行時に一度だけ初期化される不変変数、`const` はコンパイル時に確定する完全な定数。 +* **組み込み型**: `int`, `double`, `num`, `String`, `bool` が基本。文字列補間 `${}` が便利。 +* **便利な演算子**: 整数除算 `~/` や型判定 `is` によるスマートキャストを活用する。 + +次の [[./2]] では、Dartを特徴づける最も重要な機能の1つである **[[Null Safety]]** について詳しく学びます。 diff --git a/public/docs/dart/1-basics/4-1-practice1.md b/public/docs/dart/1-basics/4-1-practice1.md new file mode 100644 index 00000000..64021081 --- /dev/null +++ b/public/docs/dart/1-basics/4-1-practice1.md @@ -0,0 +1,26 @@ +--- +id: dart-basics-practice1 +title: '練習問題1: 変数と定数の使い分け' +level: 3 +question: + - constとfinalを間違えて宣言した場合、コンパイラはどのようなエラーを出しますか? + - DateTime.now() の結果をconst変数に代入できない理由を復習したいです。 +--- + +### 練習問題1: 変数と定数の使い分け + +以下の仕様を満たすDartプログラムを作成してください。 + +1. 円周率を保持する定数 `pi` を `const` で定義(値は `3.14159`)。 +2. 半径を表す変数 `radius` を `var` で定義し、初期値 `5.0` を代入。 +3. 実行時の現在時刻を保持する定数 `createdAt` を `final` で定義(`DateTime.now()` を代入)。 +4. 円の面積($\text{radius} \times \text{radius} \times \pi$)を計算し、`createdAt` と共に文字列補間を使って出力する。 + +```dart:practice1_1.dart +void main() { + // ここにコードを書いてください +} +``` + +```dart-exec:practice1_1.dart +``` diff --git a/public/docs/dart/1-basics/4-2-practice2.md b/public/docs/dart/1-basics/4-2-practice2.md new file mode 100644 index 00000000..6e6872d7 --- /dev/null +++ b/public/docs/dart/1-basics/4-2-practice2.md @@ -0,0 +1,26 @@ +--- +id: dart-basics-practice2 +title: '練習問題2: 型変換と計算' +level: 3 +question: + - double.parse() で数値に変換できない文字列を渡すと何が起きますか? + - 整数除算 ~/ と通常の除算 / の使い分けを教えてください。 +--- + +### 練習問題2: 型変換と計算 + +文字列として受け取った価格と数量から、合計金額と一人あたりの支払額を計算するプログラムを作成してください。 + +1. 文字列変数 `priceStr = "1280"` と `quantityStr = "3"` を定義する。 +2. それぞれを `int.parse()` で `int` 型に変換する。 +3. 合計金額 `total = price * quantity` を計算する。 +4. 4人で均等に割り勘した場合の「一人あたりの金額(整数部分)」を `~/` 演算子で求め、「余り(端数)」を `%` 演算子で計算して出力する。 + +```dart:practice1_2.dart +void main() { + // ここにコードを書いてください +} +``` + +```dart-exec:practice1_2.dart +``` diff --git a/public/docs/dart/10-async-stream/-intro.md b/public/docs/dart/10-async-stream/-intro.md new file mode 100644 index 00000000..9a0a6e24 --- /dev/null +++ b/public/docs/dart/10-async-stream/-intro.md @@ -0,0 +1,5 @@ +[[Future]]が「1つの値を将来返す非同期処理」であるのに対し、**`Stream`** は「時間の経過とともに**複数の値やイベントが次々と流れてくる**非同期のデータシーケンス」です。 + +WebSocketのメッセージ受信、ファイルの逐次読み込み、ボタンの連続タップイベント、センサー情報のリアルタイム受信、そして[[Flutter]]の状態管理(BLoCパターンやRxDartなど)において `Stream` は極めて重要な役割を果たします。 + +この章では、`Stream` の基本概念、`await for` による購読、`StreamController`、そして非同期ジェネレータ(`async*` / `yield`)を学びます。 diff --git a/public/docs/dart/10-async-stream/1-0-stream-concepts.md b/public/docs/dart/10-async-stream/1-0-stream-concepts.md new file mode 100644 index 00000000..f82dc555 --- /dev/null +++ b/public/docs/dart/10-async-stream/1-0-stream-concepts.md @@ -0,0 +1,32 @@ +--- +id: dart-streams-concepts +title: Stream の概念(Push型のデータフロー) +level: 2 +question: + - FutureとStreamの最も重要な違いは何ですか? + - Streamから流れてくる3種類の通知とは何ですか? +term: + - Stream + - ストリーム + - Push型 + - リアクティブ +--- + +## `Stream` の概念(Push型のデータフロー) + +**`Stream`** は、非同期に次々と発生するデータ(またはエラー)のパイプラインです。 + +購読者(リスナー)はデータが準備できたタイミングで通知を受け取ります(**Push型**)。 + +``` ++--------------------------------------------------------+ +| Stream | +| ---[ 1 ]-----[ 2 ]------[ 3 ]------| (完了 / Done) | +| 1秒後 2秒後 3秒後 | ++--------------------------------------------------------+ +``` + +Streamは以下の3つの通知をリスナーへ送信します。 +1. **データイベント(Data Event)**: 送信された値(`T`)。 +2. **エラーイベント(Error Event)**: 発生した例外。 +3. **完了イベント(Done Event)**: ストリームが終了し、これ以上データが来ない通知。 diff --git a/public/docs/dart/10-async-stream/1-1-stream-types.md b/public/docs/dart/10-async-stream/1-1-stream-types.md new file mode 100644 index 00000000..6de0e739 --- /dev/null +++ b/public/docs/dart/10-async-stream/1-1-stream-types.md @@ -0,0 +1,21 @@ +--- +id: dart-streams-types +title: 単一購読ストリームとブロードキャストストリーム +level: 3 +question: + - 単一購読ストリーム(Single-subscription)とブロードキャストストリーム(Broadcast)の違いは何ですか? + - 単一購読ストリームを複数回listenするとどうなりますか? +term: + - 単一購読ストリーム + - ブロードキャストストリーム + - asBroadcastStream +--- + +### 単一購読ストリームとブロードキャストストリーム + +DartのStreamには2つのモードが存在します。 + +* **単一購読ストリーム(Single-subscription Stream)**: + デフォルトのStream。最初から最後までのイベント順序が保証され、**同時に1つのリスナーのみ** が購読(`listen`)できます。ファイルの読み込みやHTTPレスポンスなどに適しています。 +* **ブロードキャストストリーム(Broadcast Stream)**: + **複数のリスナーが同時に購読可能** なStream。リスナーが購読を開始した瞬間以降のイベントのみを受信します。UIのクリックイベントや通知システムに適しています。`stream.asBroadcastStream()` で変換できます。 diff --git a/public/docs/dart/10-async-stream/2-0-await-for.md b/public/docs/dart/10-async-stream/2-0-await-for.md new file mode 100644 index 00000000..8ede23d3 --- /dev/null +++ b/public/docs/dart/10-async-stream/2-0-await-for.md @@ -0,0 +1,44 @@ +--- +id: dart-streams-await-for +title: await for によるStreamの購読 +level: 2 +question: + - await for 文を使うとどのようなメリットがありますか? + - await for ループはいつ終了しますか? +term: + - await for + - Stream.fromIterable +--- + +## `await for` によるStreamの購読 + +`async` 関数内では、**`await for`** ループを使って `Stream` から送られてくるデータを同期ループのように1件ずつ順次処理できます。 + +ストリームが完了(Done)するまでループが継続します。 + +```dart:await_for_demo.dart +Stream countStream(int max) async* { + for (int i = 1; i <= max; i++) { + yield i; + } +} + +void main() async { + print('Stream受信開始'); + + // await for による順次受信 + await for (final number in countStream(3)) { + print('受信データ: $number'); + } + + print('Stream完了'); +} +``` + +```dart-exec:await_for_demo.dart +Stream受信開始 +受信データ: 1 +受信データ: 2 +受信データ: 3 +Stream完了 +``` diff --git a/public/docs/dart/10-async-stream/2-1-stream-operators.md b/public/docs/dart/10-async-stream/2-1-stream-operators.md new file mode 100644 index 00000000..8ff01ecd --- /dev/null +++ b/public/docs/dart/10-async-stream/2-1-stream-operators.md @@ -0,0 +1,36 @@ +--- +id: dart-streams-operators +title: Streamの変換オペレータ(where, map) +level: 3 +question: + - Streamに対してもwhereやmapなどのオペレータを適用できますか? + - Streamを変換した結果の型はどうなりますか? +term: + - Stream変換 + - リアクティブプログラミング +--- + +### Streamの変換オペレータ(where, map) + +`Iterable` と同様に、`Stream` に対しても `where`(フィルタリング)、`map`(変換)、`take`(指定件数取得)などのパイプライン処理をメソッドチェーンで適用できます。 + +```dart:stream_operators.dart +void main() async { + final numbersStream = Stream.fromIterable([1, 2, 3, 4, 5, 6]); + + // 偶数だけを抽出し、10倍に変換して最初の2件を取得 + final transformedStream = numbersStream + .where((n) => n.isEven) + .map((n) => n * 10) + .take(2); + + await for (final val in transformedStream) { + print('変換後データ: $val'); + } +} +``` + +```dart-exec:stream_operators.dart +変換後データ: 20 +変換後データ: 40 +``` diff --git a/public/docs/dart/10-async-stream/3-0-stream-controller.md b/public/docs/dart/10-async-stream/3-0-stream-controller.md new file mode 100644 index 00000000..fe454850 --- /dev/null +++ b/public/docs/dart/10-async-stream/3-0-stream-controller.md @@ -0,0 +1,18 @@ +--- +id: dart-streams-controller +title: StreamController と StreamSubscription +level: 2 +question: + - 自分で新しいイベントをストリームに流すにはどのクラスを使いますか? + - StreamController を使い終わった後に close() を呼ぶ必要がある理由は何ですか? +term: + - StreamController + - StreamSubscription + - sink + - listen +--- + +## `StreamController` と `StreamSubscription` + +プログラム側から能動的にイベントを生成・送信(Push)したい場合は、`dart:async` ライブラリの **`StreamController`** を使用します。 +また、購読の開始と停止を制御するために **`StreamSubscription`** を管理します。 diff --git a/public/docs/dart/10-async-stream/3-1-stream-subscription.md b/public/docs/dart/10-async-stream/3-1-stream-subscription.md new file mode 100644 index 00000000..01969e7b --- /dev/null +++ b/public/docs/dart/10-async-stream/3-1-stream-subscription.md @@ -0,0 +1,53 @@ +--- +id: dart-streams-subscription +title: StreamController によるイベント送信と購読制御 +level: 3 +question: + - StreamControllerのsinkプロパティの役割は何ですか? + - StreamSubscriptionをcancel()しないと何が発生しますか(メモリリーク)? +term: + - メモリリーク + - キャンセル + - cancel +--- + +### StreamController によるイベント送信と購読制御 + +`controller.sink.add(value)` でデータを送信し、購読は `stream.listen(...)` で行います。 + +```dart:stream_controller_demo.dart +import 'dart:async'; + +void main() async { + // StreamController の作成 + final controller = StreamController(); + + // 購読を開始 (StreamSubscription を保持) + final subscription = controller.stream.listen( + (message) => print('受信: $message'), + onError: (error) => print('エラー受信: $error'), + onDone: () => print('ストリーム終了 (Done)'), + ); + + // イベントの発行 (sink経由) + controller.sink.add('第1通知: ユーザーログイン'); + controller.sink.add('第2通知: メッセージ受信'); + controller.sink.addError('第3通知: 接続不安定警告'); + + // ストリームを閉じる + await controller.close(); + + // 不要になったら購読を破棄(メモリリーク防止) + await subscription.cancel(); +} +``` + +```dart-exec:stream_controller_demo.dart +受信: 第1通知: ユーザーログイン +受信: 第2通知: メッセージ受信 +エラー受信: 第3通知: 接続不安定警告 +ストリーム終了 (Done) +``` + +> [!WARNING] +> 不要になった `StreamSubscription` は必ず `cancel()` し、`StreamController` は `close()` してメモリリークを防ぎましょう。 diff --git a/public/docs/dart/10-async-stream/4-0-async-generator.md b/public/docs/dart/10-async-stream/4-0-async-generator.md new file mode 100644 index 00000000..d3f2a135 --- /dev/null +++ b/public/docs/dart/10-async-stream/4-0-async-generator.md @@ -0,0 +1,42 @@ +--- +id: dart-streams-async-generator +title: async* と yield(非同期ジェネレータ関数) +level: 2 +question: + - sync* と async* の違いは何ですか? + - yield* の使いどころを教えてください。 +term: + - async* + - yield + - yield* + - 非同期ジェネレータ + - sync* +--- + +## `async*` と `yield`(非同期ジェネレータ関数) + +**`async*`(非同期ジェネレータ)** を使うと、関数の内部から時間の経過とともに複数の値を `yield` で順次ストリームへ送り出すことができます。 + +```dart:async_generator.dart +Stream tickStream(int count) async* { + for (int i = 1; i <= count; i++) { + await Future.delayed(const Duration(milliseconds: 50)); + yield 'Tick #$i'; + } +} + +void main() async { + await for (final tick in tickStream(3)) { + print(tick); + } +} +``` + +```dart-exec:async_generator.dart +Tick #1 +Tick #2 +Tick #3 +``` + +> [!TIP] +> 別のStream全体をそのまま委譲して流したい場合は、**`yield* otherStream;`** を使用します。 diff --git a/public/docs/dart/10-async-stream/5-0-summary.md b/public/docs/dart/10-async-stream/5-0-summary.md new file mode 100644 index 00000000..bd51a42a --- /dev/null +++ b/public/docs/dart/10-async-stream/5-0-summary.md @@ -0,0 +1,19 @@ +--- +id: dart-async-stream-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]のリアクティブプログラミングを支える `Stream` について学びました。 + +* **`Stream`**: 連続する非同期イベントを流すデータパイプライン(Push型)。 +* **単一購読 vs ブロードキャスト**: 単一購読は1つのリスナー専用、ブロードキャストは複数リスナーで共有可能。 +* **`await for`**: ストリームから流れてくる要素をループで簡潔に順次受信。 +* **ストリーム変換**: `where`, `map`, `take` などのオペレータでイベントシーケンスを加工。 +* **`StreamController`**: 能動的にイベントを発行し、`StreamSubscription` で購読とキャンセルを管理。 +* **`async*` / `yield`**: 非同期ジェネレータ関数で宣言的にストリームを生成。 + +次の [[./11]] では、実務において不可欠な **エラーハンドリング(例外処理とResult型パターン)** について学びます。 diff --git a/public/docs/dart/10-async-stream/5-1-practice1.md b/public/docs/dart/10-async-stream/5-1-practice1.md new file mode 100644 index 00000000..056f8783 --- /dev/null +++ b/public/docs/dart/10-async-stream/5-1-practice1.md @@ -0,0 +1,27 @@ +--- +id: dart-async-stream-practice1 +title: '練習問題1: async*を使ったカウントダウンストリーム' +level: 3 +question: + - async* 関数内で例外をスローするとストリームはどうなりますか? + - await for ループを途中で break した場合の挙動を教えてください。 +--- + +### 練習問題1: async*を使ったカウントダウンストリーム + +指定された秒数からゼロまでカウントダウンするストリーム関数を作成してください。 + +1. `Stream countdown(int from)` を `async*` で定義する。 +2. `from` から `0` まで1ずつ減らしながら `yield` する(各ステップで `Future.delayed(Duration(milliseconds: 50))` を待つ)。 +3. `main()` で `countdown(3)` を呼び出し、`await for` で受け取って `'残り: X秒'`、最後に `'カウントダウン終了!'` と出力する。 + +```dart:practice10_1.dart +// ここに関数を定義してください + +void main() async { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice10_1.dart +``` diff --git a/public/docs/dart/10-async-stream/5-2-practice2.md b/public/docs/dart/10-async-stream/5-2-practice2.md new file mode 100644 index 00000000..28e80879 --- /dev/null +++ b/public/docs/dart/10-async-stream/5-2-practice2.md @@ -0,0 +1,33 @@ +--- +id: dart-async-stream-practice2 +title: '練習問題2: StreamControllerを使ったイベント通知' +level: 3 +question: + - ブロードキャストストリームにするための .asBroadcastStream() の使い方を教えてください。 + - 購読リスナーが誰もいない状態でsinkにデータを送るとどうなりますか? +--- + +### 練習問題2: StreamControllerを使ったイベント通知 + +メッセージ通知システムを `StreamController` を使って構築してください。 + +1. `class NotificationHub` を作成する。 + * 内部に `StreamController` を保持する。 + * メソッド `void sendNotification(String message)` でイベントを送信する。 + * ゲッター `Stream get onNotification` でストリームを公開する。 + * メソッド `Future dispose()` でコントローラを閉じる。 +2. `main()` でインスタンスを作成し、`onNotification` を `listen` して受信ログを出力する。 +3. 2件のメッセージを送信後、`dispose()` を呼ぶ。 + +```dart:practice10_2.dart +import 'dart:async'; + +// ここにクラスを定義してください + +void main() async { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice10_2.dart +``` diff --git a/public/docs/dart/11-error-handling/-intro.md b/public/docs/dart/11-error-handling/-intro.md new file mode 100644 index 00000000..48466b7e --- /dev/null +++ b/public/docs/dart/11-error-handling/-intro.md @@ -0,0 +1,5 @@ +ソフトウェア開発において、想定外のエラーや例外的な状況を安全に処理することは、信頼性の高いアプリケーションを作るための必須要件です。 + +[[Dart]]は、JavaやC++に似た伝統的な `try-catch` / `on` 構文による例外処理機構を持つと同時に、開発時の事前条件検証を行う `assert`、そして現代的な関数型アプローチである **[[Result型]]パターン(代数的データ型とパターンマッチの活用)** を両方サポートしています。 + +この章では、Dartにおける適切なエラーハンドリング戦略を体系的に学びます。 diff --git a/public/docs/dart/11-error-handling/1-0-try-catch.md b/public/docs/dart/11-error-handling/1-0-try-catch.md new file mode 100644 index 00000000..090fa13e --- /dev/null +++ b/public/docs/dart/11-error-handling/1-0-try-catch.md @@ -0,0 +1,59 @@ +--- +id: dart-error-try-catch +title: 'try、catch、on、finally とカスタム例外' +level: 2 +question: + - catch 節の第2引数(StackTrace)の使い方は? + - rethrow キーワードはどのような場面で使用しますか? +term: + - 例外処理 + - 'on' + - rethrow + - Exception + - Error + - StackTrace +--- + +## `try`、`catch`、`on`、`finally` とカスタム例外 + +[[Dart]]では、任意のオブジェクトを例外として `throw` できますが、通常は `Exception` または `Error` を実装したクラスをスローします。 + +* **`on ExceptionType`**: 特定の例外型だけを指定してキャッチします。 +* **`catch (e, stackTrace)`**: 例外オブジェクトとスタックトレースを取得します。 +* **`rethrow`**: キャッチした例外を処理した後、再度上位の呼び出し元へ再スローします。 +* **`finally`**: 例外の有無にかかわらず、最後に必ず実行されるクリーンアップブロックです。 + +```dart:custom_exception_demo.dart +class ValidationException implements Exception { + final String message; + ValidationException(this.message); + + @override + String toString() => 'ValidationException: $message'; +} + +void validateAge(int age) { + if (age < 0) { + throw ValidationException('年齢は0以上である必要があります'); + } +} + +void main() { + try { + print('年齢チェック開始'); + validateAge(-5); + } on ValidationException catch (e) { + print('検証エラーをキャッチ: $e'); + } catch (e, stack) { + print('予期せぬエラー: $e\n$stack'); + } finally { + print('検証処理終了(finallyブロック実行)'); + } +} +``` + +```dart-exec:custom_exception_demo.dart +年齢チェック開始 +検証エラーをキャッチ: ValidationException: 年齢は0以上である必要があります +検証処理終了(finallyブロック実行) +``` diff --git a/public/docs/dart/11-error-handling/2-0-assert.md b/public/docs/dart/11-error-handling/2-0-assert.md new file mode 100644 index 00000000..85f09655 --- /dev/null +++ b/public/docs/dart/11-error-handling/2-0-assert.md @@ -0,0 +1,37 @@ +--- +id: dart-error-assert +title: assert による開発時のバグ検知 +level: 2 +question: + - assert 文は本番リリースビルド時にも実行されますか? + - assert と通常の例外スローの使い分けは何ですか? +term: + - assert + - アサーション + - デバッグモード +--- + +## `assert` による開発時のバグ検知 + +**`assert(条件, メッセージ)`** は、開発中(デバッグモード)にプログラムの不変条件や関数の前提条件を検証するための文です。 + +**リリース(AOTコンパイルや本番ビルド)時にはコード自体が完全に無視(除去)される** ため、本番環境の実行速度に影響を与えません。 + +```dart:assert_demo.dart +void setPercentage(double rate) { + // 開発時のみチェックされ、不正なら AssertionError を発生させる + assert(rate >= 0.0 && rate <= 1.0, 'rate は 0.0 〜 1.0 の間である必要があります'); + print('設定されたレート: ${(rate * 100).toStringAsFixed(1)}%'); +} + +void main() { + setPercentage(0.75); +} +``` + +```dart-exec:assert_demo.dart +設定されたレート: 75.0% +``` + +> [!TIP] +> ユーザーの入力不正など本番でも検知・回復すべきエラーには `throw Exception` を使い、プログラマの実装ミスや前提条件の違反検知には `assert` を使い分けます。 diff --git a/public/docs/dart/11-error-handling/3-0-result-pattern.md b/public/docs/dart/11-error-handling/3-0-result-pattern.md new file mode 100644 index 00000000..337ec1f6 --- /dev/null +++ b/public/docs/dart/11-error-handling/3-0-result-pattern.md @@ -0,0 +1,61 @@ +--- +id: dart-error-result-pattern +title: Result型(戻り値で成功/失敗を表現するパターン) +level: 2 +question: + - なぜ例外を投げる代わりにResult型を使うアプローチが好まれるのですか? + - sealedクラスとswitch式を使ったResult型の実装方法は? +term: + - Result型 + - 成功/失敗パターン +--- + +## Result型(戻り値で成功/失敗を表現するパターン) + +例外を `throw` するアプローチは、関数の呼び出し側がエラー処理を忘れてしまうリスクがあります。 + +Dart 3の `sealed` クラス([[./8]]参照)を活用すると、RustやSwiftのような **[[Result型]]** を型安全に自作でき、エラーハンドリングをコンパイル時に強制できます。 + +```dart:result_pattern_demo.dart +// 1. sealed クラスで Result 型を定義 +sealed class Result { + const Result(); +} + +class Success extends Result { + final T value; + const Success(this.value); +} + +class Failure extends Result { + final E error; + const Failure(this.error); +} + +// 2. 例外をスローせず Result 型を返す関数 +Result divide(int a, int b) { + if (b == 0) { + return const Failure('0 で割ることはできません'); + } + return Success(a ~/ b); +} + +void main() { + final res1 = divide(10, 2); + final res2 = divide(10, 0); + + for (final res in [res1, res2]) { + // switch式で Success と Failure を完全網羅 + final msg = switch (res) { + Success(:var value) => '計算結果: $value', + Failure(:var error) => 'エラー: $error', + }; + print(msg); + } +} +``` + +```dart-exec:result_pattern_demo.dart +計算結果: 5 +エラー: 0 で割ることはできません +``` diff --git a/public/docs/dart/11-error-handling/4-0-summary.md b/public/docs/dart/11-error-handling/4-0-summary.md new file mode 100644 index 00000000..b83a2b40 --- /dev/null +++ b/public/docs/dart/11-error-handling/4-0-summary.md @@ -0,0 +1,17 @@ +--- +id: dart-error-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]のエラーハンドリング手法について学びました。 + +* **`try-catch` / `on` / `finally`**: 型安全な例外の捕捉、再スロー(`rethrow`)、クリーンアップ処理の実行。 +* **カスタム例外**: `Exception` を実装して独自のエラー型を定義。 +* **`assert`**: 開発時・デバッグ時のみ有効な不変条件チェックでバグを早期発見。 +* **Result型パターン**: `sealed class Result` を使って成功と失敗を戻り値型で表現し、呼び出し側でのエラーハンドリング漏れを防止。 + +次の [[./12]] では、Dartチュートリアルの最終章として、並行・並列処理の仕組みである **Isolate(アイソレート)** について学びます。 diff --git a/public/docs/dart/11-error-handling/4-1-practice1.md b/public/docs/dart/11-error-handling/4-1-practice1.md new file mode 100644 index 00000000..10260870 --- /dev/null +++ b/public/docs/dart/11-error-handling/4-1-practice1.md @@ -0,0 +1,29 @@ +--- +id: dart-error-practice1 +title: '練習問題1: カスタム例外と適切な例外捕捉' +level: 3 +question: + - on節とcatch節を組み合わせる書き方を教えてください。 + - rethrowした例外はどこで捕捉されますか? +--- + +### 練習問題1: カスタム例外と適切な例外捕捉 + +パスワード強度チェック関数と、その呼び出し側のエラーハンドリングを実装してください。 + +1. `class WeakPasswordException implements Exception` を定義し、`final String reason` を保持する。 +2. `void checkPassword(String password)` 関数を定義する。 + * 文字数が8文字未満の場合、`WeakPasswordException('パスワードは8文字以上必要です')` をスローする。 + * 数字を含まない場合(`!password.contains(RegExp(r'[0-9]'))`)、`WeakPasswordException('数字を1文字以上含める必要があります')` をスローする。 +3. `main()` で `'abc'` を渡して `on WeakPasswordException` で捕捉し、理由を出力する。 + +```dart:practice11_1.dart +// ここにクラスと関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice11_1.dart +``` diff --git a/public/docs/dart/11-error-handling/4-2-practice2.md b/public/docs/dart/11-error-handling/4-2-practice2.md new file mode 100644 index 00000000..171ff6df --- /dev/null +++ b/public/docs/dart/11-error-handling/4-2-practice2.md @@ -0,0 +1,30 @@ +--- +id: dart-error-practice2 +title: '練習問題2: sealedクラスによるResult型の実装' +level: 3 +question: + - Result型に map や flatMap などのヘルパーメソッドを生やすことはできますか? + - 実務でResult型を採用するメリットを教えてください。 +--- + +### 練習問題2: sealedクラスによるResult型の実装 + +`sealed` クラスを用いた `Result` パターンを使い、安全なJSONパース関数を実装してください。 + +1. `sealed class ParseResult` を定義し、`ParseSuccess` と `ParseFailure` を作成する。 +2. `ParseResult parsePositiveInt(String input)` 関数を定義する。 + * `int.tryParse(input)` でパースを試み、失敗した場合は `ParseFailure('数値を入力してください')` を返す。 + * パースできた値が 0 以下の場合は `ParseFailure('正の整数を入力してください')` を返す。 + * 正常な正の整数の場合は `ParseSuccess(value)` を返す。 +3. `main()` で `'42'`, `'-5'`, `'abc'` を渡してテストし、`switch` 式で結果を出力する。 + +```dart:practice11_2.dart +// ここにクラスと関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice11_2.dart +``` diff --git a/public/docs/dart/12-concurrency-isolate/-intro.md b/public/docs/dart/12-concurrency-isolate/-intro.md new file mode 100644 index 00000000..8d108b2d --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/-intro.md @@ -0,0 +1,7 @@ +[[Future]]や[[Stream]]による非同期処理は、I/O待ち(通信やファイル読み書き)をノンブロッキングに行うための仕組みであり、実行自体は依然として同一のシングルスレッド(イベントループ)上で行われます。 + +そのため、画像処理、暗号化/複合、数万件に及ぶ巨大なJSONのパースなど、**CPU負荷の高い重い計算** を実行すると、UIのレンダリングがコマ落ち(ジャンク/Jank)してしまいます。 + +Dartはマルチコアプロセッサを活用して真の並列計算を行うために、メモリを共有しない独立した実行単位である **`Isolate`(アイソレート)** を提供しています。 + +この章では、Isolateの仕組み、`Isolate.run()` による簡単な並列化、そしてポートを用いた双方向通信を学びます。 diff --git a/public/docs/dart/12-concurrency-isolate/1-0-single-thread-model.md b/public/docs/dart/12-concurrency-isolate/1-0-single-thread-model.md new file mode 100644 index 00000000..9645c52b --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/1-0-single-thread-model.md @@ -0,0 +1,39 @@ +--- +id: dart-isolate-single-thread-model +title: Dartのシングルスレッドモデルとメモリ空間 +level: 2 +question: + - なぜDartはスレッド間でのメモリ共有を行わない設計なのですか? + - メモリ共有スレッドとIsolateモデルの安全性の違いは何ですか? +term: + - Isolate + - アイソレート + - メモリ分離 + - 並列処理 + - デッドロック + - レースコンディション +--- + +## Dartのシングルスレッドモデルとメモリ空間 + +一般的なマルチスレッドプログラミング(JavaやC++など)では、複数のスレッドが同一のヒープメモリ空間を共有するため、ミューテックスやロックによる排他制御が必要となり、**[[レースコンディション]](競合状態)** や **[[デッドロック]]** の温床になりがちでした。 + +[[Dart]]の **[[Isolate]](アイソレート=隔離されたもの)** は、**独自のヒープメモリと独立したイベントループを持つ完全な隔離環境** です。 + +``` ++-----------------------------+ +-----------------------------+ +| Isolate A | | Isolate B | +| +-----------------------+ | | +-----------------------+ | +| | ヒープメモリ | | | | ヒープメモリ | | +| | (オブジェクト・変数) | | | | (オブジェクト・変数) | | +| +-----------------------+ | | +-----------------------+ | +| +-----------------------+ | | +-----------------------+ | +| | イベントループ | | | | イベントループ | | +| +-----------------------+ | | +-----------------------+ | ++--------------+--------------+ +--------------+--------------+ + ^ | + | メッセージ通信 (コピー) | + +-------------------------------------+ +``` + +メモリ空間が完全に分離されているため、ロック不要で安全にマルチコアを活用した並列実行が可能です。 diff --git a/public/docs/dart/12-concurrency-isolate/2-0-isolate-intro.md b/public/docs/dart/12-concurrency-isolate/2-0-isolate-intro.md new file mode 100644 index 00000000..1a4a3562 --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/2-0-isolate-intro.md @@ -0,0 +1,47 @@ +--- +id: dart-isolate-intro +title: Isolate の概念と Isolate.run() による別スレッド処理 +level: 2 +question: + - Isolate.run() はどのような処理に向いていますか? + - Isolate.run() に渡す関数に関する制約は何ですか? +term: + - Isolate.run + - compute + - CPUバウンド +--- + +## `Isolate` の概念と `Isolate.run()` による別スレッド処理 + +Dart 2.19以降では、単発の重い計算処理([[CPUバウンド]]タスク)を別スレッドで実行して結果を受け取るための **`Isolate.run()`** が導入されました。 + +内部で別Isolateの生成、計算の実行、結果メッセージの返却、そしてIsolateの破棄までを自動で行ってくれます(Flutterの `compute()` 関数と同等です)。 + +```dart:isolate_run_demo.dart +import 'dart:isolate'; + +// 別Isolateで実行される重い計算関数 +int heavyCalculation(int count) { + int sum = 0; + for (int i = 0; i < count; i++) { + sum += i; + } + return sum; +} + +void main() async { + print('メインスレッド開始'); + + // 別スレッドで重い計算を実行 + final result = await Isolate.run(() => heavyCalculation(1000000)); + print('計算完了: $result'); + + print('メインスレッド終了'); +} +``` + +```dart-exec:isolate_run_demo.dart +メインスレッド開始 +計算完了: 499999500000 +メインスレッド終了 +``` diff --git a/public/docs/dart/12-concurrency-isolate/3-0-isolate-ports.md b/public/docs/dart/12-concurrency-isolate/3-0-isolate-ports.md new file mode 100644 index 00000000..232a258b --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/3-0-isolate-ports.md @@ -0,0 +1,55 @@ +--- +id: dart-isolate-ports +title: ポート(ReceivePort、SendPort)による双方向メッセージ通信 +level: 2 +question: + - ReceivePort と SendPort の役割の違いは何ですか? + - バックグラウンドワーカーIsolateとメインIsolateで双方向通信を行う手順は? +term: + - ReceivePort + - SendPort + - メッセージパッシング + - ポート通信 +--- + +## ポート(ReceivePort、SendPort)による双方向メッセージ通信 + +長時間常駐するバックグラウンドワーカーを作成し、メインIsolateとの間で継続的な双方向通信を行うには、**`ReceivePort`** と **`SendPort`** によるメッセージパッシングを使用します。 + +```dart:isolate_ports_demo.dart +import 'dart:isolate'; + +// ワーカーIsolateのエントリーポイント +void worker(SendPort mainSendPort) { + final workerReceivePort = ReceivePort(); + // 自身のSendPortをメインIsolateに送り返す + mainSendPort.send(workerReceivePort.sendPort); + + // メインからのメッセージを待ち受ける + workerReceivePort.listen((message) { + if (message is int) { + final squared = message * message; + mainSendPort.send('計算結果: $squared'); + } + }); +} + +void main() async { + final mainReceivePort = ReceivePort(); + + // ワーカーIsolateを起動 + await Isolate.spawn(worker, mainReceivePort.sendPort); + + // 最初に応答として送られてくるワーカーのSendPortを取得 + final workerSendPort = await mainReceivePort.first as SendPort; + + // 新たな受信ポートを作成して結果を待機 + final responsePort = ReceivePort(); + workerSendPort.send(7); + + mainReceivePort.close(); +} +``` + +> [!NOTE] +> メッセージとして送信できるデータは、プリミティブ型、コレクション、一部のシステムオブジェクトなどに限られ、Isolateを跨ぐ際にディープコピー(または転送)されます。 diff --git a/public/docs/dart/12-concurrency-isolate/4-0-summary.md b/public/docs/dart/12-concurrency-isolate/4-0-summary.md new file mode 100644 index 00000000..5dda838e --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/4-0-summary.md @@ -0,0 +1,17 @@ +--- +id: dart-isolate-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]における並列処理の仕組みである **`Isolate`** について学びました。 + +* **シングルスレッド vs Isolate**: 通常の非同期処理は同一スレッドでイベントループにより実行されるが、CPU集約型の処理は `Isolate` で別コアへ逃がす。 +* **メモリ分離(No Shared Memory)**: 各Isolateは完全に独立したヒープを持ち、ロック不要でデッドロックやレースコンディションが発生しない。 +* **`Isolate.run()`**: 単発の重い計算タスクをワンライナーで安全に別スレッド実行。 +* **`ReceivePort` / `SendPort`**: 長時間常駐するワーカーとの双方向メッセージ通信。 + +Dartチュートリアルの全章を完走したことで、言語基礎・Null Safety・関数型機能・モダンなDart 3パターン・非同期・Isolate並列処理まで、実践で活躍するための知識が身につきました! diff --git a/public/docs/dart/12-concurrency-isolate/4-1-practice1.md b/public/docs/dart/12-concurrency-isolate/4-1-practice1.md new file mode 100644 index 00000000..bd490868 --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/4-1-practice1.md @@ -0,0 +1,31 @@ +--- +id: dart-isolate-practice1 +title: '練習問題1: Isolate.run()による重い計算の並行実行' +level: 3 +question: + - Isolate.run() で返せる戻り値の型にはどのような制約がありますか? + - クロージャを Isolate.run() に渡す場合の注意点を教えてください。 +--- + +### 練習問題1: Isolate.run()による重い計算の並行実行 + +巨大なリストのソートと集計処理を `Isolate.run()` を使って別スレッドで実行するプログラムを作成してください。 + +1. `List generateAndSortNumbers(int size)` 関数を定義する。 + * 要素数 `size` のランダムな整数リストを生成し、降順ソートして先頭10件を返す。 +2. `main()` で `await Isolate.run(() => generateAndSortNumbers(500000))` を呼び出す。 +3. 取得した上位10件のリストを出力する。 + +```dart:practice12_1.dart +import 'dart:isolate'; +import 'dart:math'; + +// ここに関数を定義してください + +void main() async { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice12_1.dart +``` diff --git a/public/docs/dart/12-concurrency-isolate/4-2-practice2.md b/public/docs/dart/12-concurrency-isolate/4-2-practice2.md new file mode 100644 index 00000000..4ac5d14b --- /dev/null +++ b/public/docs/dart/12-concurrency-isolate/4-2-practice2.md @@ -0,0 +1,31 @@ +--- +id: dart-isolate-practice2 +title: '練習問題2: ポートを使った双方向ワーカーの作成' +level: 3 +question: + - Isolate間のメッセージパッシングで送信できるオブジェクトの制限は何ですか? + - バックグラウンドIsolateで例外が発生した場合のハンドリング方法は? +--- + +### 練習問題2: ポートを使った双方向ワーカーの作成 + +文字列を反転して大文字にするワーカースレッドを作成し、メッセージを送受信してください。 + +1. `void textProcessor(SendPort mainSendPort)` を定義する。 + * 自身の `ReceivePort` を作成し、その `sendPort` を `mainSendPort` に送る。 + * 送られてきた文字列を逆順(`split('').reversed.join()`)かつ大文字(`toUpperCase()`)にして送り返す。 +2. `main()` で `Isolate.spawn(textProcessor, receivePort.sendPort)` を起動する。 +3. `'hello dart'` を送信し、ワーカーから `'TRAD OLLEH'` が返ってくることを確認してポートを閉じる。 + +```dart:practice12_2.dart +import 'dart:isolate'; + +// ここに関数を定義してください + +void main() async { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice12_2.dart +``` diff --git a/public/docs/dart/2-null-safety/-intro.md b/public/docs/dart/2-null-safety/-intro.md new file mode 100644 index 00000000..b5cbe11b --- /dev/null +++ b/public/docs/dart/2-null-safety/-intro.md @@ -0,0 +1,6 @@ +[[Dart]]はバージョン2.12より **健全なNull Safety(Sound Null Safety)** を言語レベルで導入しました。 + +現代のプログラミングにおいて、`NullPointerException`(Null参照エラー)は最も頻発するバグの1つです。 +Dartの健全な[[Null Safety]]は、コードを書いている最中やコンパイル時にNullになり得る箇所を厳密に区別し、実行時クラッシュを未然に防ぎます。 + +この章では、DartのNull Safetyの仕組み、各種Null認識演算子、そして `late` 修飾子の正しい使い方をマスターします。 diff --git a/public/docs/dart/2-null-safety/1-0-types.md b/public/docs/dart/2-null-safety/1-0-types.md new file mode 100644 index 00000000..ce93c331 --- /dev/null +++ b/public/docs/dart/2-null-safety/1-0-types.md @@ -0,0 +1,41 @@ +--- +id: dart-null-safety-types +title: Nullable型(?)とNon-nullable型 +level: 2 +question: + - デフォルトで変数がNull非許容(Non-nullable)であるメリットは何ですか? + - Nullableな変数はどのように宣言しますか? +term: + - Null Safety + - null safety + - ヌル安全 + - Nullable + - Non-nullable + - null +--- + +## Nullable型(`?`)とNon-nullable型 + +[[Dart]]の型システムでは、すべての型がデフォルトで **[[Non-nullable]](Null非許容)** です。 + +値として `null` を許可したい場合のみ、型の後ろに `?` を付けて **[[Nullable]](Null許容)** として明示的に宣言します。 + +```dart:nullable_basics.dart +void main() { + // 1. Non-nullable型 (デフォルト): null を代入できない + String nonNullableText = 'Hello'; + // nonNullableText = null; // コンパイルエラー! + + // 2. Nullable型 (末尾に ? を付与): null を代入できる + String? nullableText = 'Hello'; + nullableText = null; // OK + + print('nonNullableText: $nonNullableText'); + print('nullableText: $nullableText'); +} +``` + +```dart-exec:nullable_basics.dart +nonNullableText: Hello +nullableText: null +``` diff --git a/public/docs/dart/2-null-safety/1-1-flow-analysis.md b/public/docs/dart/2-null-safety/1-1-flow-analysis.md new file mode 100644 index 00000000..819af80c --- /dev/null +++ b/public/docs/dart/2-null-safety/1-1-flow-analysis.md @@ -0,0 +1,37 @@ +--- +id: dart-null-safety-flow-analysis +title: 型プロモーション(スマートキャスト) +level: 3 +question: + - if文でnullチェックした後は自動的にNon-nullableとして扱われますか? + - フロー解析による型プロモーションが効かないケースはありますか? +term: + - 型プロモーション + - フロー解析 + - nullチェック +--- + +### 型プロモーション(スマートキャスト) + +Nullableな変数であっても、`if` 文などで `null` でないことをチェック(フロー解析)すると、そのスコープ内では自動的にNon-nullable型へと昇格(プロモート)します。 + +```dart:flow_analysis.dart +void printLength(String? text) { + if (text != null) { + // このブロック内では text は String? ではなく String として扱われる + print('文字数: ${text.length}'); + } else { + print('テキストは null です'); + } +} + +void main() { + printLength('Dart Flutter'); + printLength(null); +} +``` + +```dart-exec:flow_analysis.dart +文字数: 12 +テキストは null です +``` diff --git a/public/docs/dart/2-null-safety/2-0-null-assertion.md b/public/docs/dart/2-null-safety/2-0-null-assertion.md new file mode 100644 index 00000000..54d5ef35 --- /dev/null +++ b/public/docs/dart/2-null-safety/2-0-null-assertion.md @@ -0,0 +1,35 @@ +--- +id: dart-null-safety-assertion +title: Nullアサーション(!)とその危険性 +level: 2 +question: + - Nullアサーション演算子 (!) はどのような時に使うべきですか? + - ! を付けた変数が実際に null だった場合何が起こりますか? +term: + - Nullアサーション + - '!演算子' + - 実行時例外 +--- + +## Nullアサーション(`!`)とその危険性 + +**[[Nullアサーション]]演算子(`!`)** は、コンパイラに対して「この値はNullable型だが、実行時には絶対に `null` ではないとプログラマが保証する」と伝える演算子です。 + +```dart:null_assertion.dart +void main() { + String? maybeName = 'Alice'; + + // ! を付けることで String 型として強制的に扱う + String definiteName = maybeName!; + print('名前: $definiteName'); +} +``` + +```dart-exec:null_assertion.dart +名前: Alice +``` + +もし値が `null` であるにもかかわらず `!` を適用した場合、実行時に `TypeError` 例外が発生してプログラムがクラッシュします。 + +> [!CAUTION] +> `!` 演算子は安易に使わず、基本的には後述する **[[Null認識演算子]]** や明示的な `null` チェックを優先しましょう。 diff --git a/public/docs/dart/2-null-safety/3-0-null-aware-operators.md b/public/docs/dart/2-null-safety/3-0-null-aware-operators.md new file mode 100644 index 00000000..f5b5260c --- /dev/null +++ b/public/docs/dart/2-null-safety/3-0-null-aware-operators.md @@ -0,0 +1,21 @@ +--- +id: dart-null-safety-null-aware-operators +title: 'Null認識演算子(?.、??、??=)' +level: 2 +question: + - Null認識演算子を使うとどのようなボイラープレートコードを削減できますか? + - オプショナルチェーンとNull合流演算子の記号は何ですか? +term: + - Null認識演算子 + - null認識演算子 + - オプショナルチェーン + - '??' + - '?.' + - '??=' +--- + +## Null認識演算子(`?.`、`??`、`??=`) + +[[Dart]]には、Nullableな値を安全かつ簡潔に処理するための **[[Null認識演算子]](Null-aware operators)** が用意されています。 + +これらを活用することで、冗長な `if (x != null)` チェックを大幅に減らすことができます。 diff --git a/public/docs/dart/2-null-safety/3-1-conditional-access.md b/public/docs/dart/2-null-safety/3-1-conditional-access.md new file mode 100644 index 00000000..06db28ec --- /dev/null +++ b/public/docs/dart/2-null-safety/3-1-conditional-access.md @@ -0,0 +1,33 @@ +--- +id: dart-null-safety-conditional-access +title: 条件付きアクセス演算子(?.) +level: 3 +question: + - ?. 演算子の返り値の型は何になりますか? + - メソッド呼び出しに ?. を使う例を教えてください。 +term: + - 条件付きアクセス演算子 + - null安全な呼び出し +--- + +### 条件付きアクセス演算子(`?.`) + +対象が `null` でなければプロパティやメソッドにアクセスし、`null` であれば `null` を返します。 + +```dart:null_aware_access.dart +void main() { + String? text; + // text が null なので length にアクセスせず null を返す + int? length = text?.length; + print('length (null時): $length'); + + text = 'Hello'; + length = text?.length; + print('length (値あり時): $length'); +} +``` + +```dart-exec:null_aware_access.dart +length (null時): null +length (値あり時): 5 +``` diff --git a/public/docs/dart/2-null-safety/3-2-null-coalescing.md b/public/docs/dart/2-null-safety/3-2-null-coalescing.md new file mode 100644 index 00000000..0a8087bf --- /dev/null +++ b/public/docs/dart/2-null-safety/3-2-null-coalescing.md @@ -0,0 +1,33 @@ +--- +id: dart-null-safety-null-coalescing +title: Null合流演算子(??) +level: 3 +question: + - ?? 演算子と三項演算子の使い分けはどうなりますか? + - ?? 演算子を連続してチェーンさせることはできますか? +term: + - Null合流演算子 + - '??' + - デフォルト値 +--- + +### Null合流演算子(`??`) + +左辺が `null` でない場合は左辺の値を、`null` の場合は右辺のデフォルト値を返します。 + +```dart:null_coalescing.dart +void main() { + String? userName; + String displayName = userName ?? '名無しのユーザー'; + print('表示名: $displayName'); + + userName = 'Alice'; + displayName = userName ?? '名無しのユーザー'; + print('表示名: $displayName'); +} +``` + +```dart-exec:null_coalescing.dart +表示名: 名無しのユーザー +表示名: Alice +``` diff --git a/public/docs/dart/2-null-safety/3-3-null-assignment.md b/public/docs/dart/2-null-safety/3-3-null-assignment.md new file mode 100644 index 00000000..fe675051 --- /dev/null +++ b/public/docs/dart/2-null-safety/3-3-null-assignment.md @@ -0,0 +1,31 @@ +--- +id: dart-null-safety-null-assignment +title: Null認識代入演算子(??=) +level: 3 +question: + - ??= 演算子は変数が null でない場合はどのような動作をしますか? + - ??= 演算子を使ったキャッシュ初期化パターンの書き方は? +term: + - Null認識代入 + - '??=' +--- + +### Null認識代入演算子(`??=`) + +変数が `null` の場合のみ、右辺の値を代入します。 + +```dart:null_aware_assignment.dart +void main() { + int? count; + count ??= 10; // null なので 10 を代入 + print('count: $count'); + + count ??= 20; // 既に 10 なので 20 は代入されない + print('count: $count'); +} +``` + +```dart-exec:null_aware_assignment.dart +count: 10 +count: 10 +``` diff --git a/public/docs/dart/2-null-safety/4-0-late.md b/public/docs/dart/2-null-safety/4-0-late.md new file mode 100644 index 00000000..fcf51864 --- /dev/null +++ b/public/docs/dart/2-null-safety/4-0-late.md @@ -0,0 +1,17 @@ +--- +id: dart-null-safety-late +title: late 修飾子の仕組みと使いどころ +level: 2 +question: + - late修飾子を付けると何が変わりますか? + - late変数を初期化前に参照するとどうなりますか? +term: + - late + - late修飾子 +--- + +## `late` 修飾子の仕組みと使いどころ + +**`late` 修飾子** は、Non-nullableな変数の初期化を「宣言時ではなく、後から(あるいは必要になった時に)行う」ことをコンパイラに宣言するキーワードです。 + +宣言時点では値が決まらないNon-nullable変数の保持や、重い計算の遅延評価に利用されます。 diff --git a/public/docs/dart/2-null-safety/4-1-late-init.md b/public/docs/dart/2-null-safety/4-1-late-init.md new file mode 100644 index 00000000..d43cc9ee --- /dev/null +++ b/public/docs/dart/2-null-safety/4-1-late-init.md @@ -0,0 +1,43 @@ +--- +id: dart-null-safety-late-init +title: late による初期化の遅延とLazy評価 +level: 3 +question: + - late変数に初期化式を書いた場合、いつ実行されますか? + - LateInitializationError を防ぐための注意点は何ですか? +term: + - 遅延初期化 + - Lazy評価 + - LateInitializationError +--- + +### `late` による初期化の遅延とLazy評価 + +`late` 変数に初期化式を記述すると、その変数に**初めてアクセスされた瞬間**にのみ計算が行われます(Lazy評価)。 + +```dart:late_lazy.dart +String heavyComputation() { + print('-> 重い計算を実行中...'); + return '計算結果: 42'; +} + +void main() { + print('プログラム開始'); + late String lazyData = heavyComputation(); // この時点ではまだ実行されない + + print('データが必要になりました:'); + print(lazyData); // ここで初めて heavyComputation が呼ばれる + print('二度目の参照: $lazyData'); // 既に計算済みなので再実行はされない +} +``` + +```dart-exec:late_lazy.dart +プログラム開始 +データが必要になりました: +-> 重い計算を実行中... +計算結果: 42 +二度目の参照: 計算結果: 42 +``` + +> [!WARNING] +> 初期化式のない `late` 変数を値代入前に参照すると、`LateInitializationError` が発生します。 diff --git a/public/docs/dart/2-null-safety/5-0-summary.md b/public/docs/dart/2-null-safety/5-0-summary.md new file mode 100644 index 00000000..24703953 --- /dev/null +++ b/public/docs/dart/2-null-safety/5-0-summary.md @@ -0,0 +1,21 @@ +--- +id: dart-null-safety-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]の堅牢性を支える **[[Null Safety]]** について学びました。 + +* **デフォルトでNon-nullable**: 通常の型は `null` を代入できない。`null` を許容したい場合は `String?` のように `?` を付ける。 +* **型プロモーション**: `if (x != null)` などのチェックを行うと、自動的にNon-nullable型にプロモートされる。 +* **Nullアサーション (`!`)**: 実行時エラーのリスクがあるため、どうしても必要な場合を除き使用を避ける。 +* **Null認識演算子**: + * `?.`: `null` でない時だけプロパティ/メソッドを呼び出す。 + * `??`: `null` の場合のデフォルト値を指定する。 + * `??=`: 変数が `null` の時だけ代入する。 +* **`late` 修飾子**: Non-nullable変数の初期化を遅延させたり、初回アクセス時の遅延評価(Lazy load)を行う。 + +次の [[./3]] では、Dartにおける関数、名前付き引数、およびクロージャについて学びます。 diff --git a/public/docs/dart/2-null-safety/5-1-practice1.md b/public/docs/dart/2-null-safety/5-1-practice1.md new file mode 100644 index 00000000..b4653c83 --- /dev/null +++ b/public/docs/dart/2-null-safety/5-1-practice1.md @@ -0,0 +1,26 @@ +--- +id: dart-null-safety-practice1 +title: '練習問題1: Null安全なデータ処理' +level: 3 +question: + - nullableなオブジェクトから安全に値を取り出すベストプラクティスは何ですか? + - '?? 演算子を複数チェーンさせることはできますか?' +--- + +### 練習問題1: Null安全なデータ処理 + +ユーザープロフィールからメールアドレスのドメイン部分を取得し、取得できない場合はデフォルト値を返すプログラムを作成してください。 + +1. `String? email` を定義し、最初は `'user@example.com'` を代入する。 +2. `email` が `null` の場合は `'ドメイン不明'` を返す安全な処理を書く。 + * ヒント: `email?.split('@').last ?? 'ドメイン不明'` +3. 次に `email = null` を代入し、再度同じ処理を実行して `'ドメイン不明'` が出力されることを確認する。 + +```dart:practice2_1.dart +void main() { + // ここにコードを書いてください +} +``` + +```dart-exec:practice2_1.dart +``` diff --git a/public/docs/dart/2-null-safety/5-2-practice2.md b/public/docs/dart/2-null-safety/5-2-practice2.md new file mode 100644 index 00000000..3ac8e41a --- /dev/null +++ b/public/docs/dart/2-null-safety/5-2-practice2.md @@ -0,0 +1,26 @@ +--- +id: dart-null-safety-practice2 +title: '練習問題2: lateとNull認識演算子の活用' +level: 3 +question: + - late変数をクラス外のトップレベルで定義した場合も遅延評価されますか? + - '??= 演算子を使ってキャッシュ処理を書く場合の注意点は何ですか?' +--- + +### 練習問題2: lateとNull認識演算子の活用 + +設定マップの初期化と、値の取得・更新を行うプログラムを作成してください。 + +1. `late String appTitle` を定義し、初回アクセス時に `'My Dart App'` を生成する初期化式を記述する。 +2. `Map? config` 変数を `null` で定義する。 +3. `??=` 演算子を使って、`config` が `null` の場合に `{'theme': 'dark'}` を代入する。 +4. `appTitle` と `config` の中身を出力する。 + +```dart:practice2_2.dart +void main() { + // ここにコードを書いてください +} +``` + +```dart-exec:practice2_2.dart +``` diff --git a/public/docs/dart/3-functions/-intro.md b/public/docs/dart/3-functions/-intro.md new file mode 100644 index 00000000..69d19635 --- /dev/null +++ b/public/docs/dart/3-functions/-intro.md @@ -0,0 +1,6 @@ +[[Dart]]において、関数は単なる実行コードのまとまりではなく、真の **[[第一級オブジェクト]]** です。 + +関数を変数に代入したり、他の関数の引数として渡したり、戻り値として返したりすることができます。 +また、[[Flutter]]のWidgetツリー構築において必須となる「名前付き引数」や「アロー構文」、状態をカプセル化する「クロージャ」など、Dartの関数表現は非常に洗練されています。 + +この章では、Dartの関数の定義方法、パラメータの柔軟な指定方法、そしてクロージャの仕組みを詳しく学びます。 diff --git a/public/docs/dart/3-functions/1-0-first-class.md b/public/docs/dart/3-functions/1-0-first-class.md new file mode 100644 index 00000000..2ab901e8 --- /dev/null +++ b/public/docs/dart/3-functions/1-0-first-class.md @@ -0,0 +1,33 @@ +--- +id: dart-functions-first-class +title: 第一級オブジェクトとしての関数 +level: 2 +question: + - 関数の基本的な定義構文はどう書きますか? + - Dartで関数の型(Function型)はどのように表現しますか? +term: + - 第一級オブジェクト + - 関数型 + - Function +--- + +## 第一級オブジェクトとしての関数 + +[[Dart]]の関数は **[[第一級オブジェクト]]** であり、`Function` 型の値として変数に代入したり、高階関数の引数として渡すことができます。 + +まずは標準的な関数定義を見てみましょう。 + +```dart:function_basics.dart +int add(int a, int b) { + return a + b; +} + +void main() { + int result = add(3, 5); + print('3 + 5 = $result'); +} +``` + +```dart-exec:function_basics.dart +3 + 5 = 8 +``` diff --git a/public/docs/dart/3-functions/1-1-arrow-syntax.md b/public/docs/dart/3-functions/1-1-arrow-syntax.md new file mode 100644 index 00000000..b624b1c9 --- /dev/null +++ b/public/docs/dart/3-functions/1-1-arrow-syntax.md @@ -0,0 +1,36 @@ +--- +id: dart-functions-arrow-syntax +title: アロー構文(=>)と関数オブジェクト +level: 3 +question: + - アロー構文(=>)と通常の関数本体({})の使い分けは何ですか? + - 関数を変数に代入して呼び出す方法を教えてください。 +term: + - アロー関数 + - '=>' + - アロー構文 +--- + +### アロー構文(`=>`)と関数オブジェクト + +関数本体が単一の式(Expression)のみで構成される場合、波括弧 `{ return ...; }` の代わりに **`=>`(アロー構文)** を使って簡潔に記述できます。 + +また、関数を変数に格納して利用することもできます。 + +```dart:arrow_syntax.dart +// アロー構文による定義 +int multiply(int a, int b) => a * b; + +void main() { + print('アロー関数: ${multiply(4, 5)}'); + + // 変数に関数を代入 + int Function(int, int) op = multiply; + print('変数経由の呼び出し: ${op(10, 20)}'); +} +``` + +```dart-exec:arrow_syntax.dart +アロー関数: 20 +変数経由の呼び出し: 200 +``` diff --git a/public/docs/dart/3-functions/2-0-parameters.md b/public/docs/dart/3-functions/2-0-parameters.md new file mode 100644 index 00000000..687ce9c1 --- /dev/null +++ b/public/docs/dart/3-functions/2-0-parameters.md @@ -0,0 +1,36 @@ +--- +id: dart-functions-parameters +title: パラメータ(引数)の種類 +level: 2 +question: + - 必須の位置パラメータとは何ですか? + - オプショナルパラメータの種類には何がありますか? +term: + - 引数 + - パラメータ + - 位置パラメータ +--- + +## パラメータ(引数)の種類 + +[[Dart]]の関数のパラメータには、大きく分けて以下の2つの分類があります。 + +1. **必須の位置パラメータ(Required Positional Parameters)**: + 通常の引数。渡す順番と型が固定されます。 +2. **オプショナルパラメータ(Optional Parameters)**: + 省略可能な引数。**[[名前付き引数]](`{}`)** と **[[位置指定引数]](`[]`)** があります。 + +```dart:parameter_types.dart +// 必須の位置引数 +void greet(String name, String message) { + print('$nameさん、$message'); +} + +void main() { + greet('Alice', 'こんにちは'); +} +``` + +```dart-exec:parameter_types.dart +Aliceさん、こんにちは +``` diff --git a/public/docs/dart/3-functions/2-1-named-params.md b/public/docs/dart/3-functions/2-1-named-params.md new file mode 100644 index 00000000..dd1f686d --- /dev/null +++ b/public/docs/dart/3-functions/2-1-named-params.md @@ -0,0 +1,44 @@ +--- +id: dart-functions-named-positional +title: '名前付き引数({})と required' +level: 3 +question: + - Flutterで名前付き引数が多用される理由は何ですか? + - requiredキーワードを付けるとどうなりますか? +term: + - 名前付き引数 + - optional parameter + - required + - デフォルト引数 +--- + +### 名前付き引数(`{}`)と `required` + +引数を `{}` で囲むと、呼び出し側で引数名を明示して渡すことができるようになります。引数の順番は自由です。 + +* デフォルトで省略可能(Nullableか、デフォルト値が必要)。 +* **`required`** を付けると、名前付き引数でありながら省略不可(必須)にできます。 + +```dart:named_parameters.dart +// required で必須、デフォルト値付きで省略可能 +void createUser({ + required String username, + String role = 'member', + int? age, +}) { + print('ユーザー: $username, 権限: $role, 年齢: ${age ?? "未設定"}'); +} + +void main() { + createUser(username: 'Alice'); + createUser(role: 'admin', username: 'Bob', age: 30); +} +``` + +```dart-exec:named_parameters.dart +ユーザー: Alice, 権限: member, 年齢: 未設定 +ユーザー: Bob, 権限: admin, 年齢: 30 +``` + +> [!TIP] +> [[Flutter]]のWidgetコンストラクタはほぼすべて名前付き引数で設計されています。 diff --git a/public/docs/dart/3-functions/2-2-positional-params.md b/public/docs/dart/3-functions/2-2-positional-params.md new file mode 100644 index 00000000..0c94d8b9 --- /dev/null +++ b/public/docs/dart/3-functions/2-2-positional-params.md @@ -0,0 +1,31 @@ +--- +id: dart-functions-positional-params +title: '位置指定オプショナル引数([])' +level: 3 +question: + - 位置指定オプショナル引数の定義構文はどう書きますか? + - 名前付き引数と位置指定オプショナル引数を同時に使うことはできますか? +term: + - 位置指定引数 + - オプショナル引数 +--- + +### 位置指定オプショナル引数(`[]`) + +引数を `[]` で囲むと、順番通りのオプショナル引数を定義できます。 + +```dart:optional_positional.dart +String formatMessage(String from, String msg, [String? appName = 'MyChat']) { + return '[$appName] $from: $msg'; +} + +void main() { + print(formatMessage('Alice', 'Hello')); + print(formatMessage('Bob', 'Hi', 'FlutterApp')); +} +``` + +```dart-exec:optional_positional.dart +[MyChat] Alice: Hello +[FlutterApp] Bob: Hi +``` diff --git a/public/docs/dart/3-functions/3-0-anonymous-closures.md b/public/docs/dart/3-functions/3-0-anonymous-closures.md new file mode 100644 index 00000000..d4868a76 --- /dev/null +++ b/public/docs/dart/3-functions/3-0-anonymous-closures.md @@ -0,0 +1,17 @@ +--- +id: dart-functions-anonymous-closures +title: 無名関数とクロージャ +level: 2 +question: + - 無名関数と名前付き関数の使い分けは何ですか? + - クロージャを使うメリットは何ですか? +term: + - 無名関数 + - 匿名関数 + - クロージャ + - closure +--- + +## 無名関数とクロージャ + +[[Dart]]では、名前を持たない **[[無名関数]](ラムダ式)** を定義してコールバックとして渡したり、外側のスコープの変数を保持する **[[クロージャ]]** を作成できます。 diff --git a/public/docs/dart/3-functions/3-1-anonymous.md b/public/docs/dart/3-functions/3-1-anonymous.md new file mode 100644 index 00000000..01931a22 --- /dev/null +++ b/public/docs/dart/3-functions/3-1-anonymous.md @@ -0,0 +1,38 @@ +--- +id: dart-functions-anonymous +title: 無名関数(ラムダ式) +level: 3 +question: + - コレクションのforEachメソッドに無名関数を渡す書き方を教えてください。 + - 無名関数でアロー構文を使うことはできますか? +term: + - ラムダ式 + - コールバック関数 +--- + +### 無名関数(ラムダ式) + +関数名を付けずに定義する関数を **[[無名関数]]** と呼びます。イベントハンドラやコレクションの操作([[./4]]参照)に頻繁に利用されます。 + +```dart:anonymous_functions.dart +void main() { + final fruits = ['apple', 'banana', 'orange']; + + // (item) { ... } という無名関数を forEach に渡す + fruits.forEach((fruit) { + print('フルーツ: $fruit'); + }); + + // アロー構文を用いた無名関数 + fruits.forEach((fruit) => print('UPPER: ${fruit.toUpperCase()}')); +} +``` + +```dart-exec:anonymous_functions.dart +フルーツ: apple +フルーツ: banana +フルーツ: orange +UPPER: APPLE +UPPER: BANANA +UPPER: ORANGE +``` diff --git a/public/docs/dart/3-functions/3-2-closures.md b/public/docs/dart/3-functions/3-2-closures.md new file mode 100644 index 00000000..e9703c5e --- /dev/null +++ b/public/docs/dart/3-functions/3-2-closures.md @@ -0,0 +1,45 @@ +--- +id: dart-functions-closures +title: クロージャと変数のキャプチャ +level: 3 +question: + - クロージャ(変数のキャプチャ)とは何ですか? + - 複数のクロージャインスタンスがそれぞれ独立した状態を保持する仕組みは? +term: + - 変数のキャプチャ + - スコープ +--- + +### クロージャと変数のキャプチャ + +**[[クロージャ]]** とは、関数が定義されたスコープの外側の変数を「キャプチャ(保持)」し、関数が別のスコープで実行されてもその変数にアクセス・変更できる仕組みです。 + +```dart:closures.dart +// カウンター関数を生成する高階関数 +Function makeCounter() { + int count = 0; // クロージャによってキャプチャされるローカル変数 + + return () { + count++; + return count; + }; +} + +void main() { + final counter1 = makeCounter(); + final counter2 = makeCounter(); + + print('counter1: ${counter1()}'); // 1 + print('counter1: ${counter1()}'); // 2 + + print('counter2: ${counter2()}'); // 1 (独立した状態を持つ) + print('counter1: ${counter1()}'); // 3 +} +``` + +```dart-exec:closures.dart +counter1: 1 +counter1: 2 +counter2: 1 +counter1: 3 +``` diff --git a/public/docs/dart/3-functions/4-0-summary.md b/public/docs/dart/3-functions/4-0-summary.md new file mode 100644 index 00000000..0107298b --- /dev/null +++ b/public/docs/dart/3-functions/4-0-summary.md @@ -0,0 +1,18 @@ +--- +id: dart-functions-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]の関数システムと表現力について学びました。 + +* **第一級オブジェクト**: 関数は値として変数に代入したり、関数の引数・戻り値として扱える。 +* **アロー構文 (`=>`)**: 単一式を返す関数を簡潔に書ける。 +* **名前付き引数 (`{}`)**: 呼び出し時に引数名を明示でき、`required` で必須指定、デフォルト値設定が可能。 +* **位置指定引数 (`[]`)**: 順番に従って省略可能なオプショナル引数を定義できる。 +* **無名関数とクロージャ**: コールバック関数を即座に記述でき、外側のスコープの変数を安全にカプセル化(キャプチャ)できる。 + +次の [[./4]] では、日常の開発で多用する `List`, `Set`, `Map` などのコレクションと強力なコレクション構文について学びます。 diff --git a/public/docs/dart/3-functions/4-1-practice1.md b/public/docs/dart/3-functions/4-1-practice1.md new file mode 100644 index 00000000..8b4a4cb8 --- /dev/null +++ b/public/docs/dart/3-functions/4-1-practice1.md @@ -0,0 +1,30 @@ +--- +id: dart-functions-practice1 +title: '練習問題1: 名前付き引数を持つ計算関数' +level: 3 +question: + - 名前付き引数でデフォルト値とrequiredを組み合わせることはできますか? + - 消費税計算のような関数を設計する際のベストプラクティスを教えてください。 +--- + +### 練習問題1: 名前付き引数を持つ計算関数 + +商品の税込金額を計算する関数 `calculateTotal` を作成してください。 + +1. 関数 `calculateTotal` は以下の引数を持ちます。 + * `price`: 商品本体価格(`int`, 必須の名前付き引数 `required`) + * `taxRate`: 消費税率(`double`, デフォルト値 `0.10` の名前付き引数) + * `discount`: 割引額(`int`, デフォルト値 `0` の名前付き引数) +2. 計算式: $(\text{price} - \text{discount}) \times (1.0 + \text{taxRate})$ の整数部分(`~/ 1` または `.toInt()`)を返す。 +3. `main()` 関数で、デフォルト税率での計算と、割引・軽減税率(例: `taxRate: 0.08`)を指定した計算を実行して結果を出力する。 + +```dart:practice3_1.dart +// ここに関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice3_1.dart +``` diff --git a/public/docs/dart/3-functions/4-2-practice2.md b/public/docs/dart/3-functions/4-2-practice2.md new file mode 100644 index 00000000..4092bc80 --- /dev/null +++ b/public/docs/dart/3-functions/4-2-practice2.md @@ -0,0 +1,27 @@ +--- +id: dart-functions-practice2 +title: '練習問題2: カウンタークロージャの作成' +level: 3 +question: + - クロージャでキャプチャされた変数の寿命(ライフタイム)はどうなりますか? + - クロージャを引数として受け取る高階関数の作り方を教えてください。 +--- + +### 練習問題2: カウンタークロージャの作成 + +指定したステップ数ずつ増加するカスタムカウンターを生成する関数 `createStepCounter` を作成してください。 + +1. `int Function() createStepCounter(int step)` を定義する。 +2. 内部で初期値 `0` の変数 `current` を保持し、呼び出されるたびに `step` ずつ加算してその値を返す無名関数を返却する。 +3. `main()` でステップ数 `5` のカウンターを作成し、3回呼び出して `5`, `10`, `15` と出力されることを確認する。 + +```dart:practice3_2.dart +// ここに関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice3_2.dart +``` diff --git a/public/docs/dart/4-collections-control/-intro.md b/public/docs/dart/4-collections-control/-intro.md new file mode 100644 index 00000000..202976cb --- /dev/null +++ b/public/docs/dart/4-collections-control/-intro.md @@ -0,0 +1,6 @@ +[[Dart]]には標準で強力なコレクション(データ構造)が備わっています。 + +順序付きリスト `List`、重複を許さない集合 `Set`、キーと値のペア `Map` は、いずれも[[ジェネリクス]]によって厳密に型安全です。 +さらにDart独自の特徴として、リストやマップの定義内で直接条件分岐やループを展開できる **コレクション `if` / `for`** や **スプレッド演算子** があり、[[Flutter]]のWidgetツリーを構築する上で不可欠なテクニックとなっています。 + +この章では、コレクションの基礎から高度なデータ操作・変換までを学びます。 diff --git a/public/docs/dart/4-collections-control/1-0-collections.md b/public/docs/dart/4-collections-control/1-0-collections.md new file mode 100644 index 00000000..fb187ba8 --- /dev/null +++ b/public/docs/dart/4-collections-control/1-0-collections.md @@ -0,0 +1,20 @@ +--- +id: dart-collections-types +title: List、Set、Map とジェネリクス +level: 2 +question: + - List、Set、Map の使い分けの基準は何ですか? + - 型推論で要素の型が固定される仕組みを教えてください。 +term: + - List + - Set + - Map + - コレクション + - ジェネリクス +--- + +## `List`、`Set`、`Map` とジェネリクス + +[[Dart]]の代表的なコレクション型には、**`List`**、**`Set`**、**`Map`** があります。 + +すべて[[ジェネリクス]](``)に対応しており、要素の型がコンパイル時に厳格にチェックされます。 diff --git a/public/docs/dart/4-collections-control/1-1-list.md b/public/docs/dart/4-collections-control/1-1-list.md new file mode 100644 index 00000000..39e3f3a8 --- /dev/null +++ b/public/docs/dart/4-collections-control/1-1-list.md @@ -0,0 +1,33 @@ +--- +id: dart-collections-list +title: 'List: 順序付き配列' +level: 3 +question: + - Listのリテラル記法はどうなっていますか? + - 要素の追加やインデックスアクセスの方法を教えてください。 +term: + - 配列 + - リスト +--- + +### `List`: 順序付き配列 + +角括弧 `[]` を使ってリテラルを定義します。 + +```dart:list_basics.dart +void main() { + // 型推論により List になる + var fruits = ['apple', 'banana', 'orange']; + + fruits.add('grape'); + print('要素数: ${fruits.length}'); + print('0番目: ${fruits[0]}'); + print('全要素: $fruits'); +} +``` + +```dart-exec:list_basics.dart +要素数: 4 +0番目: apple +全要素: [apple, banana, orange, grape] +``` diff --git a/public/docs/dart/4-collections-control/1-2-set.md b/public/docs/dart/4-collections-control/1-2-set.md new file mode 100644 index 00000000..1a1a70d1 --- /dev/null +++ b/public/docs/dart/4-collections-control/1-2-set.md @@ -0,0 +1,35 @@ +--- +id: dart-collections-set +title: 'Set: 重複のない集合' +level: 3 +question: + - SetとListの違いは何ですか? + - 空のSetをリテラルで定義する際の注意点は? +term: + - 集合 + - 一意 +--- + +### `Set`: 重複のない集合 + +波括弧 `{}` を使い、要素のみをカンマ区切りで並べます。 + +```dart:set_basics.dart +void main() { + // Set + var numbers = {1, 2, 3, 2, 1}; + numbers.add(4); + numbers.add(3); // 重複は無視される + + print('Setの要素: $numbers'); + print('2を含むか: ${numbers.contains(2)}'); +} +``` + +```dart-exec:set_basics.dart +Setの要素: {1, 2, 3, 4} +2を含むか: true +``` + +> [!NOTE] +> 空の波括弧 `{}` はデフォルトで `Map` と判定されます。空のSetを作る場合は `{}` や `Set()` と型を明示します。 diff --git a/public/docs/dart/4-collections-control/1-3-map.md b/public/docs/dart/4-collections-control/1-3-map.md new file mode 100644 index 00000000..bbdeb780 --- /dev/null +++ b/public/docs/dart/4-collections-control/1-3-map.md @@ -0,0 +1,34 @@ +--- +id: dart-collections-map +title: 'Map: キーと値のペア' +level: 3 +question: + - Mapのリテラル記法はどうなっていますか? + - 存在しないキーにアクセスした場合に返る値は何ですか? +term: + - 連想配列 + - ディクショナリ +--- + +### `Map`: キーと値のペア + +波括弧 `{}` を使い、`key: value` 形式で記述します。 + +```dart:map_basics.dart +void main() { + // Map + var scores = { + 'Alice': 95, + 'Bob': 80, + }; + + scores['Charlie'] = 88; // 要素の追加・更新 + print('Aliceの点数: ${scores['Alice']}'); + print('存在しないキー: ${scores['Dave']}'); // null が返る +} +``` + +```dart-exec:map_basics.dart +Aliceの点数: 95 +存在しないキー: null +``` diff --git a/public/docs/dart/4-collections-control/2-0-collection-features.md b/public/docs/dart/4-collections-control/2-0-collection-features.md new file mode 100644 index 00000000..cf711e51 --- /dev/null +++ b/public/docs/dart/4-collections-control/2-0-collection-features.md @@ -0,0 +1,16 @@ +--- +id: dart-collections-features +title: コレクション if、for とスプレッド演算子 +level: 2 +question: + - コレクション内で直接条件分岐やループを展開する構文とは何ですか? + - FlutterのWidgetツリー構築でこれらが重宝される理由は何ですか? +term: + - コレクション構文 +--- + +## コレクション `if`、`for` とスプレッド演算子 + +[[Dart]]のコレクションリテラル内では、要素の生成ロジックとして `if`、`for`、スプレッド演算子を直接埋め込むことができます。 + +これにより、動的なリスト生成を非常に宣言的かつクリーンに記述できます。 diff --git a/public/docs/dart/4-collections-control/2-1-collection-if.md b/public/docs/dart/4-collections-control/2-1-collection-if.md new file mode 100644 index 00000000..aca1c871 --- /dev/null +++ b/public/docs/dart/4-collections-control/2-1-collection-if.md @@ -0,0 +1,34 @@ +--- +id: dart-collections-if +title: コレクション if +level: 3 +question: + - コレクション if はどのような構文で記述しますか? + - コレクション if の中で else を使ってフォールバック要素を追加できますか? +term: + - コレクションif +--- + +### コレクション `if` + +条件が `true` の場合のみ要素をコレクションに含めます。 + +```dart:collection_if.dart +void main() { + bool isAdmin = true; + bool isGuest = false; + + var navItems = [ + 'ホーム', + 'プロフィール', + if (isAdmin) '管理者パネル', + if (isGuest) 'ログイン案内', + ]; + + print(navItems); +} +``` + +```dart-exec:collection_if.dart +[ホーム, プロフィール, 管理者パネル] +``` diff --git a/public/docs/dart/4-collections-control/2-2-collection-for.md b/public/docs/dart/4-collections-control/2-2-collection-for.md new file mode 100644 index 00000000..aafddefa --- /dev/null +++ b/public/docs/dart/4-collections-control/2-2-collection-for.md @@ -0,0 +1,30 @@ +--- +id: dart-collections-for +title: コレクション for +level: 3 +question: + - コレクション for を使ってリストを展開する例を教えてください。 + - コレクション for の中でコレクション if をネストさせることはできますか? +term: + - コレクションfor +--- + +### コレクション `for` + +ループを使って複数の要素を動的に展開してコレクションを構築します。 + +```dart:collection_for.dart +void main() { + var numbers = [1, 2, 3]; + var stringList = [ + '#0', + for (var n in numbers) '#$n', + ]; + + print(stringList); +} +``` + +```dart-exec:collection_for.dart +[#0, #1, #2, #3] +``` diff --git a/public/docs/dart/4-collections-control/2-3-spread-operator.md b/public/docs/dart/4-collections-control/2-3-spread-operator.md new file mode 100644 index 00000000..44b0208f --- /dev/null +++ b/public/docs/dart/4-collections-control/2-3-spread-operator.md @@ -0,0 +1,36 @@ +--- +id: dart-collections-spread +title: 'スプレッド演算子(... / ...?)' +level: 3 +question: + - スプレッド演算子 (...) と Null認識スプレッド演算子 (...?) の使い分けは何ですか? + - リストの途中に別のリストの要素を展開する方法は? +term: + - スプレッド演算子 + - '...' + - '...?' +--- + +### スプレッド演算子(`...` / `...?`) + +既存のコレクションの全要素を別のコレクション内に展開して埋め込みます。対象が `null` になり得る場合は **`...?`(Null認識スプレッド演算子)** を使用します。 + +```dart:spread_operator.dart +void main() { + var baseList = [1, 2]; + List? extraList; // null + + var combined = [ + 0, + ...baseList, + ...?extraList, // null なので展開を安全にスキップ + 3, + ]; + + print(combined); +} +``` + +```dart-exec:spread_operator.dart +[0, 1, 2, 3] +``` diff --git a/public/docs/dart/4-collections-control/3-0-higher-order.md b/public/docs/dart/4-collections-control/3-0-higher-order.md new file mode 100644 index 00000000..73310583 --- /dev/null +++ b/public/docs/dart/4-collections-control/3-0-higher-order.md @@ -0,0 +1,15 @@ +--- +id: dart-collections-higher-order +title: 高階関数によるデータ変換 +level: 2 +question: + - Iterable と List の関係は何ですか? + - Dartで関数型スタイルのデータ変換を行うメソッドには何がありますか? +term: + - 高階関数 + - Iterable +--- + +## 高階関数によるデータ変換 + +[[Dart]]のコレクション(`Iterable`)には、関数型プログラミングスタイルでデータを操作・集計するための便利な高階メソッド(`where`, `map`, `reduce`, `fold` など)が用意されています。 diff --git a/public/docs/dart/4-collections-control/3-1-map-where.md b/public/docs/dart/4-collections-control/3-1-map-where.md new file mode 100644 index 00000000..b1285307 --- /dev/null +++ b/public/docs/dart/4-collections-control/3-1-map-where.md @@ -0,0 +1,38 @@ +--- +id: dart-collections-map-where +title: where と map による抽出・変換 +level: 3 +question: + - where メソッドと map メソッドの役割の違いは何ですか? + - map の結果を List に変換するにはどうすればよいですか? +term: + - where + - map + - toList +--- + +### `where` と `map` による抽出・変換 + +* `where`: 条件を満たす(コールバックが `true` を返す)要素のみを抽出します。 +* `map`: 各要素を変換関数で別の値に写像します。 +* `toList()`: 遅延評価される `Iterable` を `List` に確定します。 + +```dart:map_where.dart +void main() { + final numbers = [1, 2, 3, 4, 5, 6]; + + // 偶数だけを抽出し、それぞれを2乗してListに変換 + final result = numbers + .where((n) => n.isEven) + .map((n) => n * n) + .toList(); + + print('元のリスト: $numbers'); + print('変換後: $result'); +} +``` + +```dart-exec:map_where.dart +元のリスト: [1, 2, 3, 4, 5, 6] +変換後: [4, 16, 36] +``` diff --git a/public/docs/dart/4-collections-control/3-2-reduce-fold.md b/public/docs/dart/4-collections-control/3-2-reduce-fold.md new file mode 100644 index 00000000..45ad720a --- /dev/null +++ b/public/docs/dart/4-collections-control/3-2-reduce-fold.md @@ -0,0 +1,41 @@ +--- +id: dart-collections-reduce-fold +title: reduce と fold による集計 +level: 3 +question: + - reduce と fold の決定的な違いは何ですか? + - 空リストに対して reduce を呼ぶとどうなりますか? +term: + - reduce + - fold + - 畳み込み集計 +--- + +### `reduce` と `fold` による集計 + +* `reduce`: リストの先頭要素を初期値として要素を1つの値にまとめます(空リストではエラー)。 +* `fold`: 明示的な初期値を指定して集計を開始します(空リストでも安全、型変換も可能)。 + +```dart:reduce_fold.dart +void main() { + final numbers = [10, 20, 30, 40]; + + // reduce による合計 + final sum = numbers.reduce((acc, curr) => acc + curr); + print('合計 (reduce): $sum'); + + // fold による初期値 100 からの加算 + final totalWithBase = numbers.fold(100, (acc, curr) => acc + curr); + print('ベース値付き合計 (fold): $totalWithBase'); + + // fold で文字列結合 + final joined = numbers.fold('値:', (acc, curr) => '$acc $curr'); + print('文字列 (fold): $joined'); +} +``` + +```dart-exec:reduce_fold.dart +合計 (reduce): 100 +ベース値付き合計 (fold): 200 +文字列 (fold): 値: 10 20 30 40 +``` diff --git a/public/docs/dart/4-collections-control/4-0-summary.md b/public/docs/dart/4-collections-control/4-0-summary.md new file mode 100644 index 00000000..537f3899 --- /dev/null +++ b/public/docs/dart/4-collections-control/4-0-summary.md @@ -0,0 +1,17 @@ +--- +id: dart-collections-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]のコレクションと強力な制御構文について学びました。 + +* **3大コレクション**: 順序配列の `List`、一意集合の `Set`、キー値ペアの `Map` を提供。 +* **コレクション `if` / `for`**: リテラル宣言の中で条件分岐やループを展開し、宣言的なリスト生成が可能。 +* **スプレッド演算子 (`...`, `...?`)**: コレクション要素の展開や、Null安全なリスト結合を実現。 +* **高階関数**: `where`(絞り込み)、`map`(変換)、`reduce` / `fold`(集計・畳み込み)をチェーンして簡潔にデータ加工ができる。 + +次の [[./5]] では、Dart 3で導入された目玉機能である **レコード(Records)とパターンマッチング** について学びます。 diff --git a/public/docs/dart/4-collections-control/4-1-practice1.md b/public/docs/dart/4-collections-control/4-1-practice1.md new file mode 100644 index 00000000..9749141d --- /dev/null +++ b/public/docs/dart/4-collections-control/4-1-practice1.md @@ -0,0 +1,32 @@ +--- +id: dart-collections-practice1 +title: '練習問題1: コレクション操作と条件付き要素追加' +level: 3 +question: + - コレクション if の中で else を使う構文はどう書きますか? + - Setから重複を取り除いたListを作成するにはどうすればよいですか? +--- + +### 練習問題1: コレクション操作と条件付き要素追加 + +ユーザー権限と追加オプションに応じたメニュー一覧のリストを生成してください。 + +1. 以下の変数を定義する。 + * `bool isPremium = true;` + * `List? betaFeatures = ['AIアシスタント', '高速検索'];` +2. コレクション `if`、コレクション `for`、スプレッド演算子を活用して、以下の要素を持つ `List menu` を生成する。 + * `'ホーム'` + * `'設定'` + * `isPremium` が `true` の場合のみ `'プレミアム限定動画'` + * `betaFeatures` が `null` でなければその全要素を展開 + * `'ログアウト'` +3. `menu` の中身を出力する。 + +```dart:practice4_1.dart +void main() { + // ここにコードを書いてください +} +``` + +```dart-exec:practice4_1.dart +``` diff --git a/public/docs/dart/4-collections-control/4-2-practice2.md b/public/docs/dart/4-collections-control/4-2-practice2.md new file mode 100644 index 00000000..b58fe903 --- /dev/null +++ b/public/docs/dart/4-collections-control/4-2-practice2.md @@ -0,0 +1,34 @@ +--- +id: dart-collections-practice2 +title: '練習問題2: 高階関数を使った集計処理' +level: 3 +question: + - foldメソッドの初期値の型と戻り値の型の関係を教えてください。 + - メソッドチェーンで可読性を保つインデントの書き方を教えてください。 +--- + +### 練習問題2: 高階関数を使った集計処理 + +商品データのリストから条件に合う商品を抽出し、合計金額を算出するプログラムを作成してください。 + +1. 以下の商品リスト(`Map` のリスト)を用意する。 + ```dart + final items = [ + {'name': 'ノートPC', 'price': 120000, 'inStock': true}, + {'name': 'マウス', 'price': 3000, 'inStock': false}, + {'name': 'キーボード', 'price': 15000, 'inStock': true}, + {'name': 'モニター', 'price': 45000, 'inStock': true}, + ]; + ``` +2. `where` を使って在庫がある(`inStock == true`)商品のみに絞り込む。 +3. `map` で各商品の価格(`price` as `int`)を取り出す。 +4. `fold` を使って価格の総額を計算し、`'在庫あり商品の合計金額: xxx 円'` と出力する。 + +```dart:practice4_2.dart +void main() { + // ここにコードを書いてください +} +``` + +```dart-exec:practice4_2.dart +``` diff --git a/public/docs/dart/5-records-patterns/-intro.md b/public/docs/dart/5-records-patterns/-intro.md new file mode 100644 index 00000000..28667857 --- /dev/null +++ b/public/docs/dart/5-records-patterns/-intro.md @@ -0,0 +1,6 @@ +[[Dart]] 3.0で導入された最大の機能刷新が、**[[レコード]](Records)** と **[[パターンマッチング]](Pattern Matching)** です。 + +従来のオブジェクト指向言語では、複数の値をまとめて返したい場合に一時的なデータクラスを定義する必要がありました。 +Dart 3では、型安全かつ軽量なレコード型や、データの形状と値に応じて直感的に分岐・分解できるパターンマッチング、そして式として評価できる `switch` 式が利用可能になりました。 + +この章では、現代のDart開発においてコードを劇的に簡潔にするこれらの最新機能をマスターします。 diff --git a/public/docs/dart/5-records-patterns/1-0-records.md b/public/docs/dart/5-records-patterns/1-0-records.md new file mode 100644 index 00000000..0f4fccc7 --- /dev/null +++ b/public/docs/dart/5-records-patterns/1-0-records.md @@ -0,0 +1,19 @@ +--- +id: dart-records-intro +title: レコード(Records)による複数戻り値の実現 +level: 2 +question: + - レコードとクラス(Class)の違いは何ですか? + - レコードを使うことでどのようなメリットがありますか? +term: + - レコード + - Records + - record + - タプル +--- + +## レコード(Records)による複数戻り値の実現 + +**[[レコード]](Records)** は、複数の値を1つにまとめることができる匿名かつ不変(イミュータブル)な集約型(いわゆるタプル)です。 + +専用のクラスを定義することなく、関数から複数の値を型安全に返すことができます。 diff --git a/public/docs/dart/5-records-patterns/1-1-positional-records.md b/public/docs/dart/5-records-patterns/1-1-positional-records.md new file mode 100644 index 00000000..bd2f202f --- /dev/null +++ b/public/docs/dart/5-records-patterns/1-1-positional-records.md @@ -0,0 +1,33 @@ +--- +id: dart-records-positional +title: 位置指定フィールド(Positional Fields) +level: 3 +question: + - 位置指定フィールドへのアクセス方法($1, $2)はどう書きますか? + - 位置指定フィールドの型定義の構文を教えてください。 +term: + - 位置指定フィールド + - '$1' + - '$2' +--- + +### 位置指定フィールド(Positional Fields) + +丸括弧 `()` で値を囲むことでレコードを作成します。各フィールドには `$1`, `$2` でアクセスします。 + +```dart:positional_records.dart +(String, int) getUserInfo() { + return ('Alice', 25); +} + +void main() { + final user = getUserInfo(); + print('名前 (\$1): ${user.$1}'); + print('年齢 (\$2): ${user.$2}'); +} +``` + +```dart-exec:positional_records.dart +名前 ($1): Alice +年齢 ($2): 25 +``` diff --git a/public/docs/dart/5-records-patterns/1-2-named-records.md b/public/docs/dart/5-records-patterns/1-2-named-records.md new file mode 100644 index 00000000..76cdbdac --- /dev/null +++ b/public/docs/dart/5-records-patterns/1-2-named-records.md @@ -0,0 +1,33 @@ +--- +id: dart-records-named +title: 名前付きフィールド(Named Fields) +level: 3 +question: + - 名前付きフィールドを持つレコードの定義方法を教えてください。 + - レコード内で位置指定フィールドと名前付きフィールドを混在させることはできますか? +term: + - 名前付きフィールド +--- + +### 名前付きフィールド(Named Fields) + +波括弧 `{}` を使うことで、フィールドに名前を付けることができます。 + +```dart:named_records.dart +({String name, int age, bool isAdmin}) getDetailedUser() { + return (name: 'Bob', age: 30, isAdmin: true); +} + +void main() { + final user = getDetailedUser(); + print('名前: ${user.name}'); + print('年齢: ${user.age}'); + print('管理者: ${user.isAdmin}'); +} +``` + +```dart-exec:named_records.dart +名前: Bob +年齢: 30 +管理者: true +``` diff --git a/public/docs/dart/5-records-patterns/2-0-destructuring.md b/public/docs/dart/5-records-patterns/2-0-destructuring.md new file mode 100644 index 00000000..6c77cf3f --- /dev/null +++ b/public/docs/dart/5-records-patterns/2-0-destructuring.md @@ -0,0 +1,16 @@ +--- +id: dart-patterns-destructuring +title: 分解(Destructuring)によるデータの抽出 +level: 2 +question: + - パターン分解(Destructuring)とは何ですか? + - 分解代入を使うメリットは何ですか? +term: + - パターン分解 + - 分解代入 + - Destructuring +--- + +## 分解(Destructuring)によるデータの抽出 + +Dart 3のパターン構文を使用すると、レコード、List、Mapなどの複合データ構造を宣言的に分解(**[[分解代入]]**)して個別のローカル変数に抽出できます。 diff --git a/public/docs/dart/5-records-patterns/2-1-destructure-records.md b/public/docs/dart/5-records-patterns/2-1-destructure-records.md new file mode 100644 index 00000000..64e3993a --- /dev/null +++ b/public/docs/dart/5-records-patterns/2-1-destructure-records.md @@ -0,0 +1,32 @@ +--- +id: dart-patterns-destructure-records +title: レコードの分解 +level: 3 +question: + - レコードから直接変数に分解代入する方法はどう書きますか? + - 名前付きレコードの分解時にプロパティ名と同じ変数名へバインドする省略記法((:x, :y))とは? +term: + - レコード分解 +--- + +### レコードの分解 + +```dart:destructure_records.dart +(String, int) getCoords() => ('Tokyo', 100); +({int x, int y}) getPoint() => (x: 10, y: 20); + +void main() { + // 位置レコードの分解 + final (city, population) = getCoords(); + print('都市: $city, 人口: $population 万人'); + + // 名前付きレコードの分解 + final (:x, :y) = getPoint(); + print('座標: x=$x, y=$y'); +} +``` + +```dart-exec:destructure_records.dart +都市: Tokyo, 人口: 100 万人 +座標: x=10, y=20 +``` diff --git a/public/docs/dart/5-records-patterns/2-2-destructure-collections.md b/public/docs/dart/5-records-patterns/2-2-destructure-collections.md new file mode 100644 index 00000000..813d856f --- /dev/null +++ b/public/docs/dart/5-records-patterns/2-2-destructure-collections.md @@ -0,0 +1,35 @@ +--- +id: dart-patterns-destructure-collections +title: List と Map の分解 +level: 3 +question: + - リストの分解時に不要な要素をスキップするワイルドカード(_)の使い方は? + - Map の要素をパターン分解して型注釈付きで変数に抽出する方法を教えてください。 +term: + - リスト分解 + - マップ分解 + - ワイルドカード +--- + +### List と Map の分解 + +リストの要素数や中身に一致するパターンを使って値を抽出できます。不要な要素は `_`(ワイルドカード)で無視できます。 + +```dart:destructure_collections.dart +void main() { + // List の分解 + final numbers = [1, 2, 3, 4]; + final [first, second, _, fourth] = numbers; + print('1番目: $first, 2番目: $second, 4番目: $fourth'); + + // Map の分解 + final json = {'id': 'user_123', 'status': 'active'}; + final {'id': String userId, 'status': String userStatus} = json; + print('ユーザーID: $userId, ステータス: $userStatus'); +} +``` + +```dart-exec:destructure_collections.dart +1番目: 1, 2番目: 2, 4番目: 4 +ユーザーID: user_123, ステータス: active +``` diff --git a/public/docs/dart/5-records-patterns/3-0-switch-expressions.md b/public/docs/dart/5-records-patterns/3-0-switch-expressions.md new file mode 100644 index 00000000..c9ce7cf2 --- /dev/null +++ b/public/docs/dart/5-records-patterns/3-0-switch-expressions.md @@ -0,0 +1,42 @@ +--- +id: dart-patterns-switch +title: パターンマッチングと switch 式 +level: 2 +question: + - switch文とswitch式の違いは何ですか? + - switch式での網羅性チェックとは何ですか? +term: + - パターンマッチング + - switch式 + - 網羅性チェック +--- + +## パターンマッチングと `switch` 式 + +従来の `switch` 文に加え、Dart 3では評価結果の値を返す **`switch` 式(Expression)** が導入されました。 + +すべてのケースが網羅されているかコンパイラが厳密に検証する **[[網羅性チェック]]** が働きます。 + +```dart:switch_expression.dart +String describeHttpCode(int statusCode) { + return switch (statusCode) { + 200 => '成功 (OK)', + 400 => '不正なリクエスト (Bad Request)', + 404 => '未検出 (Not Found)', + 500 => 'サーバーエラー (Internal Server Error)', + _ => '不明なステータスコード ($statusCode)', + }; +} + +void main() { + print(describeHttpCode(200)); + print(describeHttpCode(404)); + print(describeHttpCode(418)); +} +``` + +```dart-exec:switch_expression.dart +成功 (OK) +未検出 (Not Found) +不明なステータスコード (418) +``` diff --git a/public/docs/dart/5-records-patterns/3-1-pattern-matching.md b/public/docs/dart/5-records-patterns/3-1-pattern-matching.md new file mode 100644 index 00000000..4b92c46c --- /dev/null +++ b/public/docs/dart/5-records-patterns/3-1-pattern-matching.md @@ -0,0 +1,45 @@ +--- +id: dart-patterns-matching-complex +title: 網羅性チェックと複雑なパターンマッチング +level: 3 +question: + - switch式の中で型判定と変数のバインドを同時に行う方法は? + - 論理演算子パターン(&& や ||)の使い方は? +term: + - 型パターン + - 論理パターン +--- + +### 網羅性チェックと複雑なパターンマッチング + +`switch` 式の中でデータの型や形状を検証しながら変数を取り出すことができます。 + +```dart:pattern_matching_complex.dart +String formatData(dynamic data) { + return switch (data) { + // 整数で 0 の場合 + 0 => 'ゼロ', + // 正の整数の場合 (関係演算子パターン) + int n && > 0 => '正の整数: $n', + // 2要素のリストの場合 + [var a, var b] => '2要素リスト: ($a, $b)', + // 特定のキーを持つマップの場合 + {'name': String name, 'age': int age} => '名前: $name, 年齢: $age', + _ => 'その他のデータ', + }; +} + +void main() { + print(formatData(10)); + print(formatData(['apple', 'banana'])); + print(formatData({'name': 'Alice', 'age': 20})); + print(formatData(false)); +} +``` + +```dart-exec:pattern_matching_complex.dart +正の整数: 10 +2要素リスト: (apple, banana) +名前: Alice, 年齢: 20 +その他のデータ +``` diff --git a/public/docs/dart/5-records-patterns/4-0-guards.md b/public/docs/dart/5-records-patterns/4-0-guards.md new file mode 100644 index 00000000..da616d2f --- /dev/null +++ b/public/docs/dart/5-records-patterns/4-0-guards.md @@ -0,0 +1,46 @@ +--- +id: dart-patterns-guards +title: Guard句(when)を使った条件分岐 +level: 2 +question: + - when句(ガード節)はパターンマッチのどの位置に記述しますか? + - when句の条件が満たされなかった場合、処理はどう流れますか? +term: + - Guard句 + - when + - ガード節 +--- + +## Guard句(`when`)を使った条件分岐 + +パターンマッチングに **`when` 節(Guard句)** を組み合わせることで、パターンの形状が一致した上でさらに任意のブール条件を課すことができます。 + +条件が `false` であれば次のマッチング候補へとフォールスルー(移動)します。 + +```dart:guards_example.dart +String evaluateScore((String, int) student) { + return switch (student) { + (var name, var score) when score == 100 => '$nameさん: 満点!素晴らしい!', + (var name, var score) when score >= 80 => '$nameさん: 優秀です (点数: $score)', + (var name, var score) when score >= 60 => '$nameさん: 合格 (点数: $score)', + (var name, var score) => '$nameさん: 要再試験 (点数: $score)', + }; +} + +void main() { + print(evaluateScore(('Alice', 100))); + print(evaluateScore(('Bob', 85))); + print(evaluateScore(('Charlie', 62))); + print(evaluateScore(('Dave', 45))); +} +``` + +```dart-exec:guards_example.dart +Aliceさん: 満点!素晴らしい! +Bobさん: 優秀です (点数: 85) +Charlieさん: 合格 (点数: 62) +Daveさん: 要再試験 (点数: 45) +``` + +> [!TIP] +> `when` 句を活用することで、ネストした `if-else` 文を平坦で美しい宣言的パターンに置き換えることができます。 diff --git a/public/docs/dart/5-records-patterns/5-0-summary.md b/public/docs/dart/5-records-patterns/5-0-summary.md new file mode 100644 index 00000000..cd8e0a61 --- /dev/null +++ b/public/docs/dart/5-records-patterns/5-0-summary.md @@ -0,0 +1,17 @@ +--- +id: dart-records-patterns-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]] 3で導入された **[[レコード]]** と **[[パターンマッチング]]** について学びました。 + +* **レコード(Records)**: クラスを自作することなく、複数の値を型安全に返却・グループ化できる軽量なデータ構造。位置指定・名前付きフィールドに対応。 +* **分解(Destructuring)**: レコード、List、Mapなどの複合データから必要な変数だけを一度に抽出できる。 +* **`switch` 式**: 式として評価され、コンパイラによる網羅性チェックが働く安全でコンパクトな条件分岐。 +* **Guard句 (`when`)**: パターンの形状一致に加えて追加のブール条件をスマートに記述できる。 + +次の [[./6]] では、オブジェクト指向プログラミングの中核である **クラスと各種コンストラクタ** について学びます。 diff --git a/public/docs/dart/5-records-patterns/5-1-practice1.md b/public/docs/dart/5-records-patterns/5-1-practice1.md new file mode 100644 index 00000000..abf6be6e --- /dev/null +++ b/public/docs/dart/5-records-patterns/5-1-practice1.md @@ -0,0 +1,27 @@ +--- +id: dart-records-patterns-practice1 +title: '練習問題1: レコードを使った座標計算' +level: 3 +question: + - 名前付きフィールドを持つレコードの型注釈はどう書きますか? + - レコードの分解代入時に型を明示することはできますか? +--- + +### 練習問題1: レコードを使った座標計算 + +2点間のマンハッタン距離と中点の座標を同時に計算して返す関数を作成してください。 + +1. 2次元座標を表すレコード型 `({int x, int y})` を引数として2つ受け取る関数 `analyzePoints` を定義する。 +2. 戻り値として、マンハッタン距離 `distance`($|x_1 - x_2| + |y_1 - y_2|$)と中点 `midpoint`(レコード `(double x, double y)`)を含む名前付きレコード `({int distance, (double, double) midpoint})` を返す。 +3. `main()` で `p1 = (x: 0, y: 0)` と `p2 = (x: 4, y: 6)` を渡して呼び出し、分解代入で結果を受け取って出力する。 + +```dart:practice5_1.dart +// ここに関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice5_1.dart +``` diff --git a/public/docs/dart/5-records-patterns/5-2-practice2.md b/public/docs/dart/5-records-patterns/5-2-practice2.md new file mode 100644 index 00000000..44b2b1cf --- /dev/null +++ b/public/docs/dart/5-records-patterns/5-2-practice2.md @@ -0,0 +1,30 @@ +--- +id: dart-records-patterns-practice2 +title: '練習問題2: switch式とパターンマッチによるコマンドパーサー' +level: 3 +question: + - switch式でListの要素数をチェックするパターンの書き方を教えてください。 + - when句を使って引数の値のバリデーションを行う例を教えてください。 +--- + +### 練習問題2: switch式とパターンマッチによるコマンドパーサー + +CLIツールに入力された文字列コマンド(引数リスト `List`)を解析する関数 `handleCommand` を作成してください。 + +1. `handleCommand(List command)` を定義し、`switch` 式を使って以下のパターンを処理する。 + * `['help']` => `'ヘルプを表示します'` + * `['view', var id]` => `'ID: $id の詳細を表示します'` + * `['create', var name, var countStr]` when `int.tryParse(countStr) != null` => `'新規作成: $name ($countStr 個)'` + * `_` => `'無効なコマンドです'` +2. `main()` でそれぞれのコマンドパターンを渡し、正しく分岐されることを確認する。 + +```dart:practice5_2.dart +// ここに関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice5_2.dart +``` diff --git a/public/docs/dart/6-classes/-intro.md b/public/docs/dart/6-classes/-intro.md new file mode 100644 index 00000000..0320f603 --- /dev/null +++ b/public/docs/dart/6-classes/-intro.md @@ -0,0 +1,6 @@ +[[Dart]]は純粋なオブジェクト指向プログラミング言語であり、すべてのデータ型はクラスを基盤としています。 + +Dartのクラス設計には、コードのボイラープレート(定型文)を大幅に削減するための強力な仕組みが揃っています。 +例えば、インスタンス変数の自動初期化糖衣構文(`this.field`)、名前付きコンストラクタ、ライブラリ単位のカプセル化、そしてインスタンス生成ロジックを柔軟に制御できる `factory` コンストラクタなどです。 + +この章では、Dartにおけるクラスの基礎から実践的なオブジェクトモデリングまでを徹底解説します。 diff --git a/public/docs/dart/6-classes/1-0-classes.md b/public/docs/dart/6-classes/1-0-classes.md new file mode 100644 index 00000000..ee4d80fd --- /dev/null +++ b/public/docs/dart/6-classes/1-0-classes.md @@ -0,0 +1,46 @@ +--- +id: dart-classes-basics +title: クラスの定義とインスタンス化(newの省略) +level: 2 +question: + - なぜDartではnewキーワードを省略できるのですか? + - コンストラクタで this.field を使う構文のメリットは何ですか? +term: + - クラス + - class + - インスタンス + - new + - メソッド +--- + +## クラスの定義とインスタンス化(`new`の省略) + +[[Dart]]でオブジェクトの設計図となる **[[クラス]](`class`)** を定義し、インスタンスを生成する基本的な構文を見てみましょう。 + +Dartでは、引数をそのままフィールドに代入する場合、`Point(this.x, this.y);` のように宣言するだけで初期化処理が完了します。また、インスタンス生成時の `new` は省略するのが標準です。 + +```dart:point_class.dart +class Point { + // フィールド + final double x; + final double y; + + // コンストラクタ(糖衣構文 this.x, this.y) + Point(this.x, this.y); + + // メソッド + void printCoordinates() { + print('Point($x, $y)'); + } +} + +void main() { + // new キーワードは省略可能 + final p1 = Point(3.0, 4.0); + p1.printCoordinates(); +} +``` + +```dart-exec:point_class.dart +Point(3.0, 4.0) +``` diff --git a/public/docs/dart/6-classes/2-0-constructors.md b/public/docs/dart/6-classes/2-0-constructors.md new file mode 100644 index 00000000..d1e6ccc1 --- /dev/null +++ b/public/docs/dart/6-classes/2-0-constructors.md @@ -0,0 +1,15 @@ +--- +id: dart-classes-constructors +title: 様々なコンストラクタ +level: 2 +question: + - Dartにはどのような種類のコンストラクタがありますか? + - 名前付きコンストラクタのメリットは何ですか? +term: + - コンストラクタ + - コンストラクタオーバーロード +--- + +## 様々なコンストラクタ + +Dartでは、1つのクラスに複数の異なる初期化方法を提供するために **[[名前付きコンストラクタ]]** や **[[リダイレクトコンストラクタ]]** を作成できます。 diff --git a/public/docs/dart/6-classes/2-1-named-redirecting.md b/public/docs/dart/6-classes/2-1-named-redirecting.md new file mode 100644 index 00000000..ec1edfc7 --- /dev/null +++ b/public/docs/dart/6-classes/2-1-named-redirecting.md @@ -0,0 +1,56 @@ +--- +id: dart-classes-named-redirecting +title: 名前付き・リダイレクト・const コンストラクタ +level: 3 +question: + - リダイレクトコンストラクタの構文はどう書きますか? + - constコンストラクタを定義するための要件は何ですか? +term: + - 名前付きコンストラクタ + - リダイレクトコンストラクタ + - constコンストラクタ +--- + +### 名前付き・リダイレクト・const コンストラクタ + +* **名前付きコンストラクタ**: `ClassName.identifier(...)` の形式で定義。 +* **リダイレクトコンストラクタ**: `: this(...)` で別コンストラクタに初期化を委譲。 +* **`const` コンストラクタ**: 全フィールドが `final` の場合、コンパイル時定数としてインスタンス化可能。 + +```dart:constructors_demo.dart +class User { + final String name; + final int age; + + // 基本コンストラクタ (const対応) + const User(this.name, this.age); + + // 名前付きコンストラクタ + User.guest() + : name = 'ゲスト', + age = 0; + + // リダイレクトコンストラクタ + User.adult(String name) : this(name, 20); + + void display() { + print('ユーザー: $name (年齢: $age)'); + } +} + +void main() { + const u1 = User('Alice', 25); + final u2 = User.guest(); + final u3 = User.adult('Bob'); + + u1.display(); + u2.display(); + u3.display(); +} +``` + +```dart-exec:constructors_demo.dart +ユーザー: Alice (年齢: 25) +ユーザー: ゲスト (年齢: 0) +ユーザー: Bob (年齢: 20) +``` diff --git a/public/docs/dart/6-classes/3-0-initializer-super.md b/public/docs/dart/6-classes/3-0-initializer-super.md new file mode 100644 index 00000000..3f1aac8e --- /dev/null +++ b/public/docs/dart/6-classes/3-0-initializer-super.md @@ -0,0 +1,15 @@ +--- +id: dart-classes-initializer-super +title: 初期化子リスト(:)と super の呼び出し +level: 2 +question: + - 初期化子リストを使う目的は何ですか? + - 親クラスのコンストラクタに引数を渡す方法は? +term: + - 初期化子リスト + - super +--- + +## 初期化子リスト(`:`)と `super` の呼び出し + +コンストラクタ実行前にフィールドの計算やバリデーションを行う **[[初期化子リスト]]** と、親クラスへの引数委譲について学びます。 diff --git a/public/docs/dart/6-classes/3-1-super-params.md b/public/docs/dart/6-classes/3-1-super-params.md new file mode 100644 index 00000000..af370f17 --- /dev/null +++ b/public/docs/dart/6-classes/3-1-super-params.md @@ -0,0 +1,45 @@ +--- +id: dart-classes-super-params +title: superパラメータと初期化子リストでのassert +level: 3 +question: + - 初期化子リスト内でassertを使うメリットは何ですか? + - super.field 構文を使うとどのようにコードが短縮されますか? +term: + - superパラメータ + - assert初期化 +--- + +### superパラメータと初期化子リストでのassert + +初期化子リスト(コロン `:` に続く記述)では、フィールドの事前計算や `assert` による引数検証が可能です。また、親クラスへの引数転送には `super.field` が使えます。 + +```dart:super_params.dart +class Shape { + final String type; + Shape(this.type); +} + +class Rectangle extends Shape { + final double width; + final double height; + final double area; + + // 初期化子リストで面積計算、バリデーション、親クラス呼出 + Rectangle(this.width, this.height) + : area = width * height, + assert(width > 0, 'width は正の数である必要があります'), + super('Rectangle'); + + void info() => print('図形: $type, 幅: $width, 高さ: $height, 面積: $area'); +} + +void main() { + final rect = Rectangle(5.0, 8.0); + rect.info(); +} +``` + +```dart-exec:super_params.dart +図形: Rectangle, 幅: 5.0, 高さ: 8.0, 面積: 40.0 +``` diff --git a/public/docs/dart/6-classes/4-0-encapsulation-getter-setter.md b/public/docs/dart/6-classes/4-0-encapsulation-getter-setter.md new file mode 100644 index 00000000..d3f2a380 --- /dev/null +++ b/public/docs/dart/6-classes/4-0-encapsulation-getter-setter.md @@ -0,0 +1,20 @@ +--- +id: dart-classes-encapsulation +title: カプセル化(_ によるプライベート化と get / set) +level: 2 +question: + - Dartには private や public キーワードがないのですか? + - アンダースコア _ でプライベート化されるスコープの単位は何ですか? +term: + - カプセル化 + - プライベート変数 + - ライブラリスコープ +--- + +## カプセル化(`_` によるプライベート化と `get` / `set`) + +[[Dart]]には `public`、`private` などのアクセス修飾子キーワードがありません。 +識別子の先頭に **アンダースコア `_`** を付けることで、その要素は **ライブラリ(同一ファイル)プライベート** になります。 + +> [!IMPORTANT] +> Dartのプライベート化は「ファイル(ライブラリ)単位」です。同一ファイル内であれば別クラスからでも `_` の付いた要素にアクセスできますが、別ファイルから `import` された場合は完全に非公開になります。 diff --git a/public/docs/dart/6-classes/4-1-private-get-set.md b/public/docs/dart/6-classes/4-1-private-get-set.md new file mode 100644 index 00000000..60b27819 --- /dev/null +++ b/public/docs/dart/6-classes/4-1-private-get-set.md @@ -0,0 +1,57 @@ +--- +id: dart-classes-private-get-set +title: ライブラリプライベートとゲッター・セッター +level: 3 +question: + - ゲッター(get)とセッター(set)の構文はどう書きますか? + - セッター内でバリデーションを行う例を教えてください。 +term: + - ゲッター + - セッター + - getter + - setter +--- + +### ライブラリプライベートとゲッター・セッター + +非公開フィールドに対して安全な読み書き手段を提供するために `get` と `set` を定義します。 + +```dart:bank_account.dart +class BankAccount { + // プライベートフィールド + double _balance = 0.0; + + // ゲッター + double get balance => _balance; + + // セッター + set balance(double value) { + if (value < 0) { + print('エラー: 残高を負の値にすることはできません'); + return; + } + _balance = value; + } + + void deposit(double amount) { + if (amount > 0) _balance += amount; + } +} + +void main() { + final account = BankAccount(); + account.deposit(5000); + print('残高: ${account.balance} 円'); + + account.balance = 8000; + print('更新後残高: ${account.balance} 円'); + + account.balance = -100; +} +``` + +```dart-exec:bank_account.dart +残高: 5000.0 円 +更新後残高: 8000.0 円 +エラー: 残高を負の値にすることはできません +``` diff --git a/public/docs/dart/6-classes/5-0-factory-constructors.md b/public/docs/dart/6-classes/5-0-factory-constructors.md new file mode 100644 index 00000000..2f3d282c --- /dev/null +++ b/public/docs/dart/6-classes/5-0-factory-constructors.md @@ -0,0 +1,16 @@ +--- +id: dart-classes-factory +title: factory コンストラクタ(シングルトンやJSONパースの実装) +level: 2 +question: + - 通常のコンストラクタと factory コンストラクタの決定的な違いは何ですか? + - factory コンストラクタを使う典型的なユースケースは何ですか? +term: + - factoryコンストラクタ + - factory + - ファクトリコンストラクタ +--- + +## `factory` コンストラクタ(シングルトンやJSONパースの実装) + +通常のコンストラクタは常に新しいインスタンスを生成しますが、**`factory` コンストラクタ** を使うと、既存のキャッシュ済みインスタンスを返したり、サブクラスのインスタンスを生成して返すなど、生成ロジックを柔軟に制御できます。 diff --git a/public/docs/dart/6-classes/5-1-singleton.md b/public/docs/dart/6-classes/5-1-singleton.md new file mode 100644 index 00000000..b8b5ebbe --- /dev/null +++ b/public/docs/dart/6-classes/5-1-singleton.md @@ -0,0 +1,41 @@ +--- +id: dart-classes-singleton +title: シングルトンパターンの実装 +level: 3 +question: + - factory コンストラクタを使ってシングルトンパターンを実装する方法を教えてください。 + - identical() 関数の役割は何ですか? +term: + - シングルトン + - identical +--- + +### シングルトンパターンの実装 + +`factory` コンストラクタを使って、常に同一のプライベート静的インスタンスを返却することで、シングルトンをエレガントに実装できます。 + +```dart:singleton_demo.dart +class DatabaseService { + final String dbName; + static DatabaseService? _instance; + + DatabaseService._internal(this.dbName); + + // factory コンストラクタ: 常に同一のインスタンスを返す + factory DatabaseService({String dbName = 'main.db'}) { + _instance ??= DatabaseService._internal(dbName); + return _instance!; + } +} + +void main() { + final db1 = DatabaseService(); + final db2 = DatabaseService(); + + print('同一インスタンスか: ${identical(db1, db2)}'); +} +``` + +```dart-exec:singleton_demo.dart +同一インスタンスか: true +``` diff --git a/public/docs/dart/6-classes/5-2-from-json.md b/public/docs/dart/6-classes/5-2-from-json.md new file mode 100644 index 00000000..2934358b --- /dev/null +++ b/public/docs/dart/6-classes/5-2-from-json.md @@ -0,0 +1,48 @@ +--- +id: dart-classes-from-json +title: JSONパース用 fromJson ファクトリ +level: 3 +question: + - fromJson ファクトリコンストラクタの典型的なシグネチャを教えてください。 + - Mapから安全にキャストしてフィールドを初期化する書き方は? +term: + - fromJson + - JSONパース +--- + +### JSONパース用 `fromJson` ファクトリ + +`Map` からオブジェクトを生成する `fromJson` を `factory` コンストラクタとして定義するのがDartのデファクトスタンダードです。 + +```dart:factory_from_json.dart +class Product { + final String id; + final String title; + final int price; + + const Product({ + required this.id, + required this.title, + required this.price, + }); + + factory Product.fromJson(Map json) { + return Product( + id: json['id'] as String, + title: json['title'] as String, + price: json['price'] as int, + ); + } +} + +void main() { + final jsonMap = {'id': 'p_01', 'title': 'メカニカルキーボード', 'price': 14800}; + final product = Product.fromJson(jsonMap); + + print('商品: ${product.title} (¥${product.price})'); +} +``` + +```dart-exec:factory_from_json.dart +商品: メカニカルキーボード (¥14800) +``` diff --git a/public/docs/dart/6-classes/6-0-summary.md b/public/docs/dart/6-classes/6-0-summary.md new file mode 100644 index 00000000..82344dc3 --- /dev/null +++ b/public/docs/dart/6-classes/6-0-summary.md @@ -0,0 +1,18 @@ +--- +id: dart-classes-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]のオブジェクト指向の根幹となるクラスとコンストラクタについて学びました。 + +* **クラスとインスタンス化**: `this.field` による簡潔な初期化が可能で、`new` キーワードは省略するのが標準。 +* **多彩なコンストラクタ**: 名前付きコンストラクタ(`User.guest()`)、リダイレクトコンストラクタ(`: this(...)`)、定数生成のための `const` コンストラクタ。 +* **初期化子リストと `super`**: コロン `:` に続くフィールド事前計算やバリデーション、`super.field` による親クラスへの引数転送。 +* **カプセル化**: アンダースコア `_` でライブラリ単位のプライベート化を実現し、`get` / `set` で安全にプロパティを公開。 +* **`factory` コンストラクタ**: キャッシュ済みインスタンスの返却(シングルトン)や、`fromJson` などのカスタム生成ロジックを実現。 + +次の [[./7]] では、継承、インターフェース、Mixin、拡張メソッド(Extension)による **クラスの拡張と高度な設計パターン** を学びます。 diff --git a/public/docs/dart/6-classes/6-1-practice1.md b/public/docs/dart/6-classes/6-1-practice1.md new file mode 100644 index 00000000..a69eecbe --- /dev/null +++ b/public/docs/dart/6-classes/6-1-practice1.md @@ -0,0 +1,29 @@ +--- +id: dart-classes-practice1 +title: '練習問題1: カプセル化されたBankAccountクラス' +level: 3 +question: + - ゲッターのみを定義して読み取り専用プロパティを作るメリットは何ですか? + - コンストラクタで初期残高を受け取りつつ、負の値を防ぐにはどうすればよいですか? +--- + +### 練習問題1: カプセル化されたBankAccountクラス + +口座名義と残高を管理する `BankAccount` クラスを実装してください。 + +1. プライベート変数 `String _owner` と `int _balance` を持つ。 +2. コンストラクタ `BankAccount(this._owner, this._balance)` を定義する(初期残高が負でないことを `assert` または初期化子リストで確認)。 +3. 読み取り専用ゲッター `owner` と `balance` を定義する。 +4. メソッド `void withdraw(int amount)` を定義し、残高が十分な場合は引き落としを行い、不足している場合は `'残高不足です'` と出力する。 +5. `main()` でインスタンスを生成し、引き落とし処理をテストする。 + +```dart:practice6_1.dart +// ここにクラスを定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice6_1.dart +``` diff --git a/public/docs/dart/6-classes/6-2-practice2.md b/public/docs/dart/6-classes/6-2-practice2.md new file mode 100644 index 00000000..fd17f5d1 --- /dev/null +++ b/public/docs/dart/6-classes/6-2-practice2.md @@ -0,0 +1,29 @@ +--- +id: dart-classes-practice2 +title: '練習問題2: factoryコンストラクタによるJSONパース' +level: 3 +question: + - JSONパースでnullが渡される可能性がある場合の安全な書き方を教えてください。 + - toString() メソッドをオーバーライドするメリットは何ですか? +--- + +### 練習問題2: factoryコンストラクタによるJSONパース + +Web APIから受信したユーザー設定データを表す `UserSettings` クラスを実装してください。 + +1. フィールドとして `final String theme`(テーマ名)と `final bool notificationsEnabled`(通知有無)を持つ。 +2. 通常のコンストラクタ `const UserSettings({required this.theme, required this.notificationsEnabled});` を定義する。 +3. `factory UserSettings.fromJson(Map json)` を実装し、Mapから各プロパティをパースしてインスタンスを返す。 + * `theme` が未指定の場合は `'light'`、`notificationsEnabled` が未指定の場合は `true` をデフォルト値とする。 +4. `main()` でJSONマップを渡し、パースされた設定内容を出力する。 + +```dart:practice6_2.dart +// ここにクラスを定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice6_2.dart +``` diff --git a/public/docs/dart/7-class-extension/-intro.md b/public/docs/dart/7-class-extension/-intro.md new file mode 100644 index 00000000..56bf2ee9 --- /dev/null +++ b/public/docs/dart/7-class-extension/-intro.md @@ -0,0 +1,5 @@ +[[Dart]]のクラス設計は、単なる単一継承にとどまらず、柔軟で再利用性の高いコード構造を実現するための高度な機能を豊富に備えています。 + +すべてのクラスが暗黙的にインターフェースとして機能する「暗黙的インターフェース」、多重継承のメリットを安全に享受できる「Mixin(ミキシン)」、既存のSDKクラスに新しいメソッドを生やすことができる「拡張メソッド(Extension)」、そしてメソッドやプロパティを持てる「Enhanced Enum」などです。 + +この章では、Dartならではのオブジェクト設計のベストプラクティスを学びます。 diff --git a/public/docs/dart/7-class-extension/1-0-extends-implements.md b/public/docs/dart/7-class-extension/1-0-extends-implements.md new file mode 100644 index 00000000..fe46cf84 --- /dev/null +++ b/public/docs/dart/7-class-extension/1-0-extends-implements.md @@ -0,0 +1,18 @@ +--- +id: dart-classes-extends-implements +title: extends(継承)と implements(インターフェース実装)の違い +level: 2 +question: + - なぜDartには interface キーワードが別途不要だったのですか? + - 暗黙的インターフェースとは何ですか? +term: + - extends + - 継承 + - implements + - 暗黙的インターフェース + - インターフェース +--- + +## `extends`(継承)と `implements`(インターフェース実装)の違い + +[[Dart]]では、すべてのクラスが自動的に **[[暗黙的インターフェース]](Implicit Interface)** を定義しています。これにより、任意のクラスを `implements` の対象として利用できます。 diff --git a/public/docs/dart/7-class-extension/1-1-extends-implements-details.md b/public/docs/dart/7-class-extension/1-1-extends-implements-details.md new file mode 100644 index 00000000..2bbb80d8 --- /dev/null +++ b/public/docs/dart/7-class-extension/1-1-extends-implements-details.md @@ -0,0 +1,54 @@ +--- +id: dart-classes-extends-implements-details +title: 単一継承と暗黙的インターフェースの使い分け +level: 3 +question: + - implements を使った場合、親クラスの実装コードは引き継がれますか? + - 多重継承が禁止されている一方で implements を複数指定できる理由は何ですか? +term: + - '@override' + - オーバーライド +--- + +### 単一継承と暗黙的インターフェースの使い分け + +* **`extends` (単一継承)**: 親クラスの実装(コード)を引き継ぎます。 +* **`implements` (インターフェース実装)**: 親クラスの型シグネチャのみを引き継ぎ、全メソッドを自前で `@override` 再実装します(複数指定可能)。 + +```dart:extends_implements.dart +class Animal { + void speak() { + print('動物が鳴きます'); + } +} + +// 1. extends: 実装を引き継ぎオーバーライド +class Dog extends Animal { + @override + void speak() { + print('ワンワン!'); + } +} + +// 2. implements: 型のみ流用し全メソッドを自前実装 +class RobotDog implements Animal { + @override + void speak() { + print('ビープ!ワンワン (電子音)'); + } +} + +void makeNoise(Animal animal) { + animal.speak(); +} + +void main() { + makeNoise(Dog()); + makeNoise(RobotDog()); +} +``` + +```dart-exec:extends_implements.dart +ワンワン! +ビープ!ワンワン (電子音) +``` diff --git a/public/docs/dart/7-class-extension/2-0-mixins.md b/public/docs/dart/7-class-extension/2-0-mixins.md new file mode 100644 index 00000000..df2216be --- /dev/null +++ b/public/docs/dart/7-class-extension/2-0-mixins.md @@ -0,0 +1,57 @@ +--- +id: dart-classes-mixins +title: Mixin(mixin、with)による機能の注入 +level: 2 +question: + - Mixinと継承の違いは何ですか? + - 複数のMixinを with で組み合わせる方法を教えてください。 +term: + - Mixin + - mixin + - with + - 機能の注入 +--- + +## Mixin(`mixin`、`with`)による機能の注入 + +**[[Mixin]]** は、クラス階層に縛られることなく、複数のクラス間で実装コード(メソッドやプロパティ)を共有・注入するための仕組みです。 + +`mixin` キーワードで定義し、クラスに `with` キーワードで適用します。 + +```dart:mixins_demo.dart +// 1. ロギング機能を提供する Mixin +mixin Logger { + void log(String message) { + print('[LOG ${DateTime.now().hour}:${DateTime.now().minute}] $message'); + } +} + +// 2. 永続化機能を提供する Mixin +mixin Serializable { + Map toJson(); +} + +// クラスに with で複数の Mixin を合成 +class AppUser with Logger, Serializable { + final String name; + AppUser(this.name); + + void login() { + log('ユーザー $name がログインしました'); + } + + @override + Map toJson() => {'name': name}; +} + +void main() { + final user = AppUser('Alice'); + user.login(); + print('JSON: ${user.toJson()}'); +} +``` + +```dart-exec:mixins_demo.dart +[LOG 5:20] ユーザー Alice がログインしました +JSON: {name: Alice} +``` diff --git a/public/docs/dart/7-class-extension/3-0-extension-methods.md b/public/docs/dart/7-class-extension/3-0-extension-methods.md new file mode 100644 index 00000000..c32f5e7f --- /dev/null +++ b/public/docs/dart/7-class-extension/3-0-extension-methods.md @@ -0,0 +1,46 @@ +--- +id: dart-classes-extensions +title: Extension(拡張メソッド)による既存クラスへの機能追加 +level: 2 +question: + - 拡張メソッドを使うと標準ライブラリのクラス(Stringやintなど)にメソッドを追加できますか? + - 拡張メソッドの定義構文はどう書きますか? +term: + - Extension + - 拡張メソッド + - extension + - extension on +--- + +## Extension(拡張メソッド)による既存クラスへの機能追加 + +**[[Extension]](拡張メソッド)** を使うと、既存のクラス(Dartの組み込み型やサードパーティ製ライブラリのクラス)のソースコードを変更することなく、新しいメソッドやゲッター、演算子を追加できます。 + +構文は `extension ExtensionName on TargetType { ... }` です。 + +```dart:extension_methods.dart +// String クラスに拡張メソッドを追加 +extension StringExtensions on String { + // ゲッター: 先頭文字を大文字にする + String get capitalize { + if (isEmpty) return this; + return this[0].toUpperCase() + substring(1); + } + + // メソッド: 安全に整数に変換する + int? toIntOrNull() => int.tryParse(this); +} + +void main() { + String word = 'flutter'; + print('capitalize: ${word.capitalize}'); + + String numStr = '42'; + print('toIntOrNull: ${numStr.toIntOrNull()}'); +} +``` + +```dart-exec:extension_methods.dart +capitalize: Flutter +toIntOrNull: 42 +``` diff --git a/public/docs/dart/7-class-extension/4-0-enhanced-enums.md b/public/docs/dart/7-class-extension/4-0-enhanced-enums.md new file mode 100644 index 00000000..fa61f556 --- /dev/null +++ b/public/docs/dart/7-class-extension/4-0-enhanced-enums.md @@ -0,0 +1,54 @@ +--- +id: dart-classes-enhanced-enums +title: 高機能な列挙型(Enhanced Enum) +level: 2 +question: + - 通常のenumとEnhanced Enumの違いは何ですか? + - Enumにフィールドやメソッド、コンストラクタを持たせるにはどう書きますか? +term: + - Enum + - 列挙型 + - Enhanced Enum + - enum +--- + +## 高機能な列挙型(Enhanced Enum) + +Dart 2.17以降の **[[Enhanced Enum]]** では、列挙型の各値にフィールド(値)、コンストラクタ、ゲッター、メソッドを持たせることができます。 + +単なる定数の列挙を超えて、型安全で表現力豊かなドメインモデルを簡潔に定義できます。 + +```dart:enhanced_enums.dart +enum HttpStatus { + ok(200, 'Success'), + notFound(404, 'Not Found'), + serverError(500, 'Internal Server Error'); + + // フィールド + final int code; + final String description; + + // const コンストラクタ + const HttpStatus(this.code, this.description); + + // ゲッター + bool get isSuccess => code >= 200 && code < 300; + + // メソッド + void log() { + print('[$code] $description (成功: $isSuccess)'); + } +} + +void main() { + final status = HttpStatus.notFound; + status.log(); + + print('HttpStatus.ok isSuccess: ${HttpStatus.ok.isSuccess}'); +} +``` + +```dart-exec:enhanced_enums.dart +[404] Not Found (成功: false) +HttpStatus.ok isSuccess: true +``` diff --git a/public/docs/dart/7-class-extension/5-0-summary.md b/public/docs/dart/7-class-extension/5-0-summary.md new file mode 100644 index 00000000..b430f2cb --- /dev/null +++ b/public/docs/dart/7-class-extension/5-0-summary.md @@ -0,0 +1,17 @@ +--- +id: dart-class-extension-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]の高度なオブジェクト指向設計機能について学びました。 + +* **`extends` vs `implements`**: `extends` は実装コードを引き継ぎ、`implements` は暗黙的インターフェースの型契約のみを満たす。 +* **Mixin (`mixin`, `with`)**: 継承階層とは直交して、複数クラスへ横断的な機能・振る舞いを注入する。 +* **Extension (`extension on`)**: 既存のクラスを変更することなく、便利メソッドやプロパティを外付けで拡張する。 +* **Enhanced Enum**: フィールド、コンストラクタ、メソッドを持つ高機能な列挙型。 + +次の [[./8]] では、Dart 3で導入された `sealed`, `base`, `interface`, `final` などの **クラス修飾子** を使った厳密なドメインモデリングを学びます。 diff --git a/public/docs/dart/7-class-extension/5-1-practice1.md b/public/docs/dart/7-class-extension/5-1-practice1.md new file mode 100644 index 00000000..634a439d --- /dev/null +++ b/public/docs/dart/7-class-extension/5-1-practice1.md @@ -0,0 +1,28 @@ +--- +id: dart-class-extension-practice1 +title: '練習問題1: Mixinを用いたロギング機能の追加' +level: 3 +question: + - Mixinの中で自身を特定のクラス型として扱うにはどうしますか? + - クラス名を取得する runtimeType プロパティの使い方を教えてください。 +--- + +### 練習問題1: Mixinを用いたロギング機能の追加 + +イベントの発生ログを出力する `PrintableLogger` Mixin を作成し、クラスに適用してください。 + +1. `mixin PrintableLogger` を定義する。 + * メソッド `void logEvent(String eventName)` を持ち、`'[${runtimeType}] イベント発生: $eventName'` と出力する。 +2. `class OrderService with PrintableLogger` を定義し、メソッド `void checkout(String itemId)` 内で `logEvent('注文完了: $itemId')` を呼び出す。 +3. `main()` で `OrderService` のインスタンスを作成し、`checkout('item_999')` を実行する。 + +```dart:practice7_1.dart +// ここにコードを書いてください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice7_1.dart +``` diff --git a/public/docs/dart/7-class-extension/5-2-practice2.md b/public/docs/dart/7-class-extension/5-2-practice2.md new file mode 100644 index 00000000..4b3c11df --- /dev/null +++ b/public/docs/dart/7-class-extension/5-2-practice2.md @@ -0,0 +1,28 @@ +--- +id: dart-class-extension-practice2 +title: '練習問題2: Extensionを用いた文字列ユーティリティ' +level: 3 +question: + - Extensionのスコープ(ライブラリ内でのみ有効かエクスポート可能か)について教えてください。 + - 拡張メソッド内で元のオブジェクト(this)を変更することはできますか? +--- + +### 練習問題2: Extensionを用いた文字列ユーティリティ + +`String` クラスに対して、特定の文字列操作を行う拡張メソッドを実装してください。 + +1. `extension StringUtils on String` を定義する。 + * ゲッター `bool get isEmail`: 文字列に `@` と `.` が含まれているかを簡易判定する。 + * メソッド `String mask({int visibleCount = 2})`: 先頭から `visibleCount` 文字だけ残し、以降を `*` でマスクした文字列を返す(文字数が `visibleCount` 以下の場合はそのまま返す)。 +2. `main()` で `'user@example.com'` に対する `isEmail` の判定結果と、`mask()` によるマスク文字列(例: `'us**************'`)を出力する。 + +```dart:practice7_2.dart +// ここにコードを書いてください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice7_2.dart +``` diff --git a/public/docs/dart/8-class-modifiers/-intro.md b/public/docs/dart/8-class-modifiers/-intro.md new file mode 100644 index 00000000..dca9658a --- /dev/null +++ b/public/docs/dart/8-class-modifiers/-intro.md @@ -0,0 +1,5 @@ +[[Dart]] 3.0では、ライブラリの作者がクラスの継承・実装・インスタンス化の権限をきめ細かく制御できるように、**[[クラス修飾子]](Class Modifiers)** が導入されました。 + +特に **`sealed` クラス** は、RustのEnumやKotlinのSealed Class、TypeScriptのTagged Union(判別可能なUnion型)に相当する **[[代数的データ型]](ADT)** をDartで美しく実現し、`switch` 式と組み合わせることで網羅性チェックの恩恵を最大限に引き出します。 + +この章では、`sealed`, `base`, `interface`, `final` 修飾子の役割と、堅牢なドメインモデル設計を学びます。 diff --git a/public/docs/dart/8-class-modifiers/1-0-sealed-classes.md b/public/docs/dart/8-class-modifiers/1-0-sealed-classes.md new file mode 100644 index 00000000..f39561ac --- /dev/null +++ b/public/docs/dart/8-class-modifiers/1-0-sealed-classes.md @@ -0,0 +1,68 @@ +--- +id: dart-class-modifiers-sealed +title: sealed クラスによる代数的データ型(ADT)とSwitchの組み合わせ +level: 2 +question: + - sealedクラスを定義する主な目的は何ですか? + - sealedクラスのサブクラスはどこで定義する必要がありますか? + - switch式でsealedクラスの全パターンを網羅しないとどうなりますか? +term: + - sealed + - sealedクラス + - 代数的データ型 + - ADT + - 網羅性 +--- + +## `sealed` クラスによる代数的データ型(ADT)とSwitchの組み合わせ + +**`sealed` クラス** は、そのクラスを直接インスタンス化できず(暗黙的に `abstract`)、**サブクラスの定義を同一ライブラリ(同一ファイル)内のみに限定する** 修飾子です。 + +コンパイラは「そのクラスのサブクラスの全パターン」を完全に把握できるため、`switch` 式で**[[網羅性チェック]]**が働きます。 + +```dart:sealed_demo.dart +// 1. sealed クラスでUI状態の基底クラスを定義 +sealed class UiState {} + +class InitialState extends UiState {} +class LoadingState extends UiState {} +class SuccessState extends UiState { + final List data; + SuccessState(this.data); +} +class ErrorState extends UiState { + final String message; + ErrorState(this.message); +} + +// 2. switch式で状態に応じたレンダリング文字列を生成 +String render(UiState state) { + // 全サブクラスが網羅されているため、default (_) 節が不要! + return switch (state) { + InitialState() => '待機中...', + LoadingState() => '読み込み中...', + SuccessState(:var data) => 'データ取得成功: ${data.join(', ')}', + ErrorState(:var message) => 'エラー発生: $message', + }; +} + +void main() { + UiState state = LoadingState(); + print(render(state)); + + state = SuccessState(['Dart', 'Flutter']); + print(render(state)); + + state = ErrorState('ネットワーク接続に失敗しました'); + print(render(state)); +} +``` + +```dart-exec:sealed_demo.dart +読み込み中... +データ取得成功: Dart, Flutter +エラー発生: ネットワーク接続に失敗しました +``` + +> [!TIP] +> 新しい状態を後から追加した際、`switch` 式でそのケースを書き忘れていると、コンパイラがビルド時に即座に未網羅エラーを通知してくれます。 diff --git a/public/docs/dart/8-class-modifiers/2-0-other-modifiers.md b/public/docs/dart/8-class-modifiers/2-0-other-modifiers.md new file mode 100644 index 00000000..cf08f13d --- /dev/null +++ b/public/docs/dart/8-class-modifiers/2-0-other-modifiers.md @@ -0,0 +1,25 @@ +--- +id: dart-class-modifiers-base-interface-final +title: base、interface、final 修飾子の使い分け +level: 2 +question: + - クラス修飾子を導入する目的は何ですか? + - 各修飾子の制約の違いの概要を教えてください。 +term: + - base + - interface修飾子 + - final修飾子 + - クラス修飾子 +--- + +## `base`、`interface`、`final` 修飾子の使い分け + +[[Dart]] 3では、ライブラリ境界外(外部パッケージや別ファイル)からのクラス利用方法を制限するために、各種クラス修飾子が提供されています。 + +| 修飾子 | 外部でのインスタンス化 | 外部での `extends` (継承) | 外部での `implements` (実装) | 外部での `with` (Mixin) | +| :--- | :---: | :---: | :---: | :---: | +| **`class` (無印)** | ○ | ○ | ○ | × | +| **`base`** | ○ | **○ (`base` 必須)** | × | × | +| **`interface`** | ○ | × | **○** | × | +| **`final`** | ○ | × | × | × | +| **`sealed`** | × (abstract) | × (同ファイル内のみ) | × (同ファイル内のみ) | × | diff --git a/public/docs/dart/8-class-modifiers/2-1-base-modifier.md b/public/docs/dart/8-class-modifiers/2-1-base-modifier.md new file mode 100644 index 00000000..afc52f5f --- /dev/null +++ b/public/docs/dart/8-class-modifiers/2-1-base-modifier.md @@ -0,0 +1,25 @@ +--- +id: dart-class-modifiers-base +title: base 修飾子(継承のみを許可し implements を禁止) +level: 3 +question: + - base修飾子を付けると外部パッケージでどのような制約が課されますか? + - baseクラスを継承するサブクラスにもbaseやfinalの指定が必要ですか? +term: + - base +--- + +### `base` 修飾子(継承のみを許可し implements を禁止) + +クラスに新しいメソッドを追加しても外部のサブクラスが壊れないように設計したい場合に利用します。外部ライブラリからの暗黙的インターフェース実装(`implements`)を禁止します。 + +```dart +// 外部ライブラリ側 +base class Vehicle { + void move() => print('移動中'); +} + +// 利用側 +// class Car implements Vehicle {} // エラー: implements 不可 +base class Car extends Vehicle {} // OK: extends のみ許可 +``` diff --git a/public/docs/dart/8-class-modifiers/2-2-interface-modifier.md b/public/docs/dart/8-class-modifiers/2-2-interface-modifier.md new file mode 100644 index 00000000..35c12be5 --- /dev/null +++ b/public/docs/dart/8-class-modifiers/2-2-interface-modifier.md @@ -0,0 +1,29 @@ +--- +id: dart-class-modifiers-interface +title: interface 修飾子(実装のみを許可し継承を禁止) +level: 3 +question: + - interface修飾子と通常のclassの違いは何ですか? + - 外部で extends を禁止して implements のみを強制するメリットは何ですか? +term: + - interface + - interface修飾子 +--- + +### `interface` 修飾子(実装のみを許可し継承を禁止) + +APIの型シグネチャのみを提供し、内部実装の継承による暗黙の依存を防ぎたい場合に利用します。外部ライブラリからの継承(`extends`)を禁止し、実装(`implements`)のみを許可します。 + +```dart +// 外部ライブラリ側 +interface class StorageService { + void save(String key, String value) {} +} + +// 利用側 +// class LocalStorage extends StorageService {} // エラー: extends 不可 +class LocalStorage implements StorageService { // OK: implements のみ許可 + @override + void save(String key, String value) => print('Saved $key'); +} +``` diff --git a/public/docs/dart/8-class-modifiers/2-3-final-modifier.md b/public/docs/dart/8-class-modifiers/2-3-final-modifier.md new file mode 100644 index 00000000..2a7e7605 --- /dev/null +++ b/public/docs/dart/8-class-modifiers/2-3-final-modifier.md @@ -0,0 +1,26 @@ +--- +id: dart-class-modifiers-final +title: final 修飾子(外部からの継承・実装を完全に禁止) +level: 3 +question: + - final修飾子をクラスに付けると何が禁止されますか? + - クラスの動作を完全に不変に保ちたい場合にfinalを使う理由は何ですか? +term: + - final修飾子 +--- + +### `final` 修飾子(外部からの継承・実装を完全に禁止) + +クラスの動作を完全に固定し、外部ライブラリからのサブクラス化(継承 `extends` および実装 `implements` の両方)を一切許さない場合に使用します。 + +```dart +// 外部ライブラリ側 +final class ImmutableConfig { + final String env; + const ImmutableConfig(this.env); +} + +// 利用側 +// class MyConfig extends ImmutableConfig {} // エラー: extends 不可 +// class MyConfig implements ImmutableConfig {} // エラー: implements 不可 +``` diff --git a/public/docs/dart/8-class-modifiers/3-0-domain-modeling.md b/public/docs/dart/8-class-modifiers/3-0-domain-modeling.md new file mode 100644 index 00000000..a07d0838 --- /dev/null +++ b/public/docs/dart/8-class-modifiers/3-0-domain-modeling.md @@ -0,0 +1,65 @@ +--- +id: dart-class-modifiers-domain-modeling +title: ドメインモデルの堅牢な設計方法 +level: 2 +question: + - アプリケーションの状態管理でsealedクラスを活用するベストプラクティスは何ですか? + - 不正な状態の表現を型レベルで不可能にするにはどうすればよいですか? +term: + - ドメインモデル + - 状態モデリング + - イミュータブル + - 型安全 +--- + +## ドメインモデルの堅牢な設計方法 + +クラス修飾子(特に `sealed`)とDart 3のパターンマッチングを組み合わせることで、**「不正な状態を型レベルで表現不可能にする」** 堅牢な[[ドメインモデル]]が構築できます。 + +```dart:auth_state_modeling.dart +sealed class AuthState { + const AuthState(); +} + +class AuthUnauthenticated extends AuthState { + const AuthUnauthenticated(); +} + +class AuthAuthenticating extends AuthState { + const AuthAuthenticating(); +} + +class AuthAuthenticated extends AuthState { + final String userId; + final String token; + const AuthAuthenticated({required this.userId, required this.token}); +} + +class AuthError extends AuthState { + final String errorMessage; + const AuthError(this.errorMessage); +} + +void printAuthAction(AuthState state) { + final action = switch (state) { + AuthUnauthenticated() => 'ログインボタンを表示します', + AuthAuthenticating() => 'スピナーを表示して待機します', + AuthAuthenticated(:var userId) => 'ユーザー $userId のマイページを表示します', + AuthError(:var errorMessage) => 'エラーダイアログを表示: $errorMessage', + }; + print(action); +} + +void main() { + AuthState state = const AuthAuthenticating(); + printAuthAction(state); + + state = const AuthAuthenticated(userId: 'u_777', token: 'jwt_abc123'); + printAuthAction(state); +} +``` + +```dart-exec:auth_state_modeling.dart +スピナーを表示して待機します +ユーザー u_777 のマイページを表示します +``` diff --git a/public/docs/dart/8-class-modifiers/4-0-summary.md b/public/docs/dart/8-class-modifiers/4-0-summary.md new file mode 100644 index 00000000..06039277 --- /dev/null +++ b/public/docs/dart/8-class-modifiers/4-0-summary.md @@ -0,0 +1,18 @@ +--- +id: dart-class-modifiers-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]] 3のクラス修飾子によるモデリングの厳密化について学びました。 + +* **`sealed` クラス**: 同一ファイル内のみでサブクラス定義を制限し、代数的データ型(ADT)と `switch` 式の完全な網羅性チェックを実現。 +* **`base` 修飾子**: 継承のみを許可し、暗黙的インターフェースとしての `implements` を防ぐ。 +* **`interface` 修飾子**: インターフェース実装(`implements`)のみを許可し、実装の継承を防ぐ。 +* **`final` 修飾子**: 外部からの継承と実装を両方禁止する。 +* **ドメインモデリング**: 状態をフラグではなく型として分割定義し、不正な状態を排除する設計手法。 + +次の [[./9]] では、Dartのイベント駆動アーキテクチャの中核である **非同期処理の基礎(Futureとasync/await)** を学びます。 diff --git a/public/docs/dart/8-class-modifiers/4-1-practice1.md b/public/docs/dart/8-class-modifiers/4-1-practice1.md new file mode 100644 index 00000000..04612113 --- /dev/null +++ b/public/docs/dart/8-class-modifiers/4-1-practice1.md @@ -0,0 +1,32 @@ +--- +id: dart-class-modifiers-practice1 +title: '練習問題1: sealedクラスを用いたUI状態のモデリング' +level: 3 +question: + - sealedクラスを基底にした場合のジェネリクス型の指定方法はどうなりますか? + - switch式で各サブクラスのフィールドをパターンマッチで取り出す際の注意点は? +--- + +### 練習問題1: sealedクラスを用いたUI状態のモデリング + +天気予報アプリの画面状態を表す `WeatherState` を `sealed` クラスで実装してください。 + +1. `sealed class WeatherState` を定義する。 +2. 以下のサブクラスを作成する。 + * `class WeatherInitial extends WeatherState` + * `class WeatherLoading extends WeatherState` + * `class WeatherSuccess extends WeatherState`: フィールド `final String city`, `final double temperature` を持つ。 + * `class WeatherFailure extends WeatherState`: フィールド `final String error` を持つ。 +3. `String getWeatherMessage(WeatherState state)` 関数を `switch` 式で実装し、全状態に応じた適切なメッセージ文字列を返す。 +4. `main()` で各状態を作成し、メッセージを出力する。 + +```dart:practice8_1.dart +// ここにクラスと関数を定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice8_1.dart +``` diff --git a/public/docs/dart/8-class-modifiers/4-2-practice2.md b/public/docs/dart/8-class-modifiers/4-2-practice2.md new file mode 100644 index 00000000..42805fe3 --- /dev/null +++ b/public/docs/dart/8-class-modifiers/4-2-practice2.md @@ -0,0 +1,28 @@ +--- +id: dart-class-modifiers-practice2 +title: '練習問題2: クラス修飾子による安全なAPI設計' +level: 3 +question: + - abstract interface class と書くことの意味は何ですか? + - baseクラスを継承するクラスに付けなければならない修飾子のルールを復習したいです。 +--- + +### 練習問題2: クラス修飾子による安全なAPI設計 + +キャッシュストレージのインターフェースと、基底となるローカルストレージ実装を設計してください。 + +1. `abstract interface class CacheRepository` を定義し、メソッド `T? get(String key);` と `void set(String key, T value);` を宣言する。 +2. `base class MemoryCacheRepository implements CacheRepository` を実装し、内部の `Map` にデータを保存・取得する処理を実装する。 +3. `final class ExpiringMemoryCache extends MemoryCacheRepository` を定義する。 +4. `main()` で `ExpiringMemoryCache` を生成し、データの追加と取得をテストする。 + +```dart:practice8_2.dart +// ここにクラスを定義してください + +void main() { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice8_2.dart +``` diff --git a/public/docs/dart/9-async-future/-intro.md b/public/docs/dart/9-async-future/-intro.md new file mode 100644 index 00000000..6c4cde33 --- /dev/null +++ b/public/docs/dart/9-async-future/-intro.md @@ -0,0 +1,5 @@ +ネットワーク通信、データベース読み書き、ファイルの入出力など、完了までに時間のかかる処理をUIスレッドで同期的に実行すると、アプリ画面がフリーズしてしまいます。 + +[[Dart]]はシングルスレッドのイベントループモデルを採用しており、処理をブロックすることなく非同期にタスクを実行するための仕組みとして **`Future`** と **`async` / `await`** を提供しています。 + +この章では、Dartのイベントループの概念から、非同期関数の書き方、エラーハンドリングまでを学びます。 diff --git a/public/docs/dart/9-async-future/1-0-event-loop.md b/public/docs/dart/9-async-future/1-0-event-loop.md new file mode 100644 index 00000000..90ea1e21 --- /dev/null +++ b/public/docs/dart/9-async-future/1-0-event-loop.md @@ -0,0 +1,39 @@ +--- +id: dart-async-event-loop +title: 同期処理と非同期処理の違い(イベントループの概念) +level: 2 +question: + - Dartのシングルスレッドモデルで非同期処理が並行して進む仕組みは何ですか? + - イベントキューとマイクロタスクキューの違いは何ですか? +term: + - イベントループ + - マイクロタスク + - イベントキュー + - 非同期処理 + - シングルスレッド +--- + +## 同期処理と非同期処理の違い(イベントループの概念) + +[[Dart]]コードは単一のスレッド(**[[シングルスレッド]]**)上で動作します。処理が重い計算でスレッドを長時間占有すると、描画やタップ入力がブロックされてしまいます。 + +Dartはこの問題を **[[イベントループ]](Event Loop)** によって解決しています。 + +``` + +----------------------------+ + | 現在実行中のコード | + +-------------+--------------+ + | (完了) + v ++---------------------------+---------------------------+ +| イベントループ (Event Loop) | +| | +| 1. マイクロタスクキュー (Microtask Queue) - 最優先 | +| [ Microtask 1 ] -> [ Microtask 2 ] | +| | +| 2. イベントキュー (Event Queue) - 通常の非同期タスク | +| [ I/O完了 ] -> [ タイマー発火 ] -> [ タップイベント ] | ++-------------------------------------------------------+ +``` + +非同期処理(`Future`)をスケジューリングすると、現在のコードの実行が終わった後、イベントループが順番にそれを取り出して実行します。 diff --git a/public/docs/dart/9-async-future/2-0-future.md b/public/docs/dart/9-async-future/2-0-future.md new file mode 100644 index 00000000..e6c876d6 --- /dev/null +++ b/public/docs/dart/9-async-future/2-0-future.md @@ -0,0 +1,16 @@ +--- +id: dart-async-future +title: Future の仕組み +level: 2 +question: + - Futureとは具体的に何を表すオブジェクトですか? + - JavaScriptのPromiseとFutureの共通点は何ですか? +term: + - Future + - Future.value + - Future.delayed +--- + +## `Future` の仕組み + +**`Future`** は、「将来のある時点で値 `T` またはエラーを返す非同期処理の結果」を表すオブジェクトです(JavaScriptの `Promise` や Rustの `Future` に相当します)。 diff --git a/public/docs/dart/9-async-future/2-1-future-details.md b/public/docs/dart/9-async-future/2-1-future-details.md new file mode 100644 index 00000000..75609902 --- /dev/null +++ b/public/docs/dart/9-async-future/2-1-future-details.md @@ -0,0 +1,47 @@ +--- +id: dart-async-future-details +title: Futureの3つの状態と then() による購読 +level: 3 +question: + - Futureの3つの状態(Uncompleted, Completed with data, Completed with error)とは? + - then() と catchError() を使ったコールバック記法はどう書きますか? +term: + - then + - catchError + - コールバック +--- + +### Futureの3つの状態と `then()` による購読 + +Futureには以下の3つの状態があります。 + +1. **未完了(Uncompleted)**: 非同期処理が実行中で、まだ結果が出ていない状態。 +2. **値完了(Completed with data)**: 処理が正常に完了し、値が得られた状態。 +3. **エラー完了(Completed with error)**: 例外が発生して失敗した状態。 + +```dart:future_basics.dart +Future fetchUserGreeting() { + return Future.delayed( + const Duration(milliseconds: 100), + () => 'こんにちは、ユーザーさん!', + ); +} + +void main() { + print('1. 処理開始'); + + fetchUserGreeting().then((greeting) { + print('3. データ受信: $greeting'); + }).catchError((error) { + print('エラー: $error'); + }); + + print('2. メイン関数の同期処理完了'); +} +``` + +```dart-exec:future_basics.dart +1. 処理開始 +2. メイン関数の同期処理完了 +3. データ受信: こんにちは、ユーザーさん! +``` diff --git a/public/docs/dart/9-async-future/3-0-async-await.md b/public/docs/dart/9-async-future/3-0-async-await.md new file mode 100644 index 00000000..b890b4a2 --- /dev/null +++ b/public/docs/dart/9-async-future/3-0-async-await.md @@ -0,0 +1,47 @@ +--- +id: dart-async-async-await +title: async / await による可読性の高い非同期コード +level: 2 +question: + - asyncキーワードを付けた関数の戻り値型は何になりますか? + - awaitキーワードはどこで使用できますか? +term: + - async + - await + - async/await +--- + +## `async` / `await` による可読性の高い非同期コード + +**`async`** と **`await`** キーワードを使うことで、非同期コードを同期コードと同じような直線的で読みやすいフローで記述できます。 + +* 関数宣言の後に `async` を付けると、その関数は自動的に `Future` を返す非同期関数になります。 +* `async` 関数内でのみ、`Future` の完了を待機する **`await`** 式が使えます。 + +```dart:async_await_demo.dart +Future fetchUserId() async { + await Future.delayed(const Duration(milliseconds: 50)); + return 42; +} + +Future fetchUserName(int id) async { + await Future.delayed(const Duration(milliseconds: 50)); + return 'Alice (ID: $id)'; +} + +Future displayUser() async { + print('ユーザー情報を取得中...'); + final id = await fetchUserId(); + final name = await fetchUserName(id); + print('取得完了: $name'); +} + +void main() async { + await displayUser(); +} +``` + +```dart-exec:async_await_demo.dart +ユーザー情報を取得中... +取得完了: Alice (ID: 42) +``` diff --git a/public/docs/dart/9-async-future/3-1-future-wait.md b/public/docs/dart/9-async-future/3-1-future-wait.md new file mode 100644 index 00000000..b2716ff4 --- /dev/null +++ b/public/docs/dart/9-async-future/3-1-future-wait.md @@ -0,0 +1,36 @@ +--- +id: dart-async-future-wait +title: Future.wait による複数の非同期処理の並行実行 +level: 3 +question: + - Future.waitを使って複数の非同期タスクを並行実行する方法を教えてください。 + - 並行実行と直列実行で所要時間はどう変化しますか? +term: + - Future.wait + - 並行実行 +--- + +### `Future.wait` による複数の非同期処理の並行実行 + +互いに依存しない複数の非同期処理を同時に実行したい場合は、`Future.wait` を使います。 + +```dart:future_wait.dart +Future fetchPostTitle() async => 'Dart 3の紹介'; +Future fetchLikeCount() async => 128; + +void main() async { + // 並行して実行し、両方の完了を待つ + final results = await Future.wait([ + fetchPostTitle(), + fetchLikeCount(), + ]); + + print('タイトル: ${results[0]}'); + print('いいね数: ${results[1]}'); +} +``` + +```dart-exec:future_wait.dart +タイトル: Dart 3の紹介 +いいね数: 128 +``` diff --git a/public/docs/dart/9-async-future/4-0-async-error-handling.md b/public/docs/dart/9-async-future/4-0-async-error-handling.md new file mode 100644 index 00000000..b4ca3d48 --- /dev/null +++ b/public/docs/dart/9-async-future/4-0-async-error-handling.md @@ -0,0 +1,58 @@ +--- +id: dart-async-error-handling +title: 非同期処理のエラーハンドリング(try-catch-finally) +level: 2 +question: + - async/await でのエラーハンドリングは通常の try-catch と同じですか? + - 非同期例外を確実に捕捉するための注意点は何ですか? +term: + - 非同期エラーハンドリング + - try-catch-finally + - try + - catch + - finally +--- + +## 非同期処理のエラーハンドリング(`try-catch-finally`) + +`async` / `await` を使った非同期処理では、同期コードと全く同じ **`try-catch-finally`** 構文で例外を捕捉できます。 + +```dart:async_try_catch.dart +Future loadDataFromServer({required bool shouldFail}) async { + await Future.delayed(const Duration(milliseconds: 50)); + if (shouldFail) { + throw Exception('ネットワーク接続が切断されました'); + } + return '正常データレスポンス'; +} + +Future handleRequest(bool shouldFail) async { + try { + print('リクエスト送信...'); + final data = await loadDataFromServer(shouldFail: shouldFail); + print('成功: $data'); + } catch (e) { + print('例外をキャッチ: $e'); + } finally { + print('リソースクリーンアップ完了\n'); + } +} + +void main() async { + await handleRequest(false); + await handleRequest(true); +} +``` + +```dart-exec:async_try_catch.dart +リクエスト送信... +成功: 正常データレスポンス +リソースクリーンアップ完了 + +リクエスト送信... +例外をキャッチ: Exception: ネットワーク接続が切断されました +リソースクリーンアップ完了 +``` + +> [!TIP] +> `await` を付け忘れた `Future` で例外が発生すると、`try-catch` ブロックをすり抜けて未処理の非同期例外となるため注意してください。 diff --git a/public/docs/dart/9-async-future/5-0-summary.md b/public/docs/dart/9-async-future/5-0-summary.md new file mode 100644 index 00000000..f39ed56d --- /dev/null +++ b/public/docs/dart/9-async-future/5-0-summary.md @@ -0,0 +1,18 @@ +--- +id: dart-async-future-summary +title: この章のまとめ +level: 2 +question: [] +--- + +## この章のまとめ + +この章では、[[Dart]]における非同期処理の基礎について学びました。 + +* **シングルスレッドとイベントループ**: DartはイベントループによってUIスレッドを止めずに非同期タスクを実行する。 +* **`Future`**: 将来確定する単一の非同期処理結果を表すオブジェクト。 +* **`async` / `await`**: 直感的な構文で非同期コードを同期コードのように直線的に記述できる。 +* **`Future.wait`**: 複数の非同期処理を並行して実行し、すべての完了を待機する。 +* **非同期エラー処理**: `try-catch-finally` 構文で同期処理と同様に例外をハンドリングできる。 + +次の [[./10]] では、単一の値ではなく「時間の経過とともに連続して発生するイベントのストリーム」を扱う **`Stream`** について学びます。 diff --git a/public/docs/dart/9-async-future/5-1-practice1.md b/public/docs/dart/9-async-future/5-1-practice1.md new file mode 100644 index 00000000..209bd511 --- /dev/null +++ b/public/docs/dart/9-async-future/5-1-practice1.md @@ -0,0 +1,29 @@ +--- +id: dart-async-future-practice1 +title: '練習問題1: 非同期データフェッチのシミュレーション' +level: 3 +question: + - Future.delayed を使ったモック関数の作成方法を教えてください。 + - 非同期関数が例外をスローした場合の try-catch の動作を復習したいです。 +--- + +### 練習問題1: 非同期データフェッチのシミュレーション + +サーバーからユーザーデータを非同期に取得する関数 `fetchUser` を作成してください。 + +1. `Future> fetchUser(int id)` を定義する。 +2. `Future.delayed(Duration(milliseconds: 100))` で遅延を発生させる。 +3. `id <= 0` の場合は `Exception('無効なユーザーIDです: $id')` をスローする。 +4. 正しいIDの場合は `{'id': id, 'name': 'User_$id', 'points': 1500}` を返す。 +5. `main()` で正常系(`id: 1`)と異常系(`id: -1`)の呼び出しを `try-catch` で処理し、結果を出力する。 + +```dart:practice9_1.dart +// ここに関数を定義してください + +void main() async { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice9_1.dart +``` diff --git a/public/docs/dart/9-async-future/5-2-practice2.md b/public/docs/dart/9-async-future/5-2-practice2.md new file mode 100644 index 00000000..32e20ff8 --- /dev/null +++ b/public/docs/dart/9-async-future/5-2-practice2.md @@ -0,0 +1,28 @@ +--- +id: dart-async-future-practice2 +title: '練習問題2: 複数の非同期タスクの並行処理' +level: 3 +question: + - Future.wait で複数の異なる戻り値型を持つ処理をまとめる方法は? + - 非同期処理を並行実行する際の例外ハンドリングの注意点は何ですか? +--- + +### 練習問題2: 複数の非同期タスクの並行処理 + +商品の価格情報と在庫情報を並行してフェッチし、合算結果を出力するプログラムを作成してください。 + +1. `Future fetchPrice(String productId)`: 50ミリ秒後に価格 `3500` を返す。 +2. `Future fetchStock(String productId)`: 50ミリ秒後に在庫数 `12` を返す。 +3. `main()` で `Future.wait` を使って2つの処理を同時に実行し、結果をレコードまたはリストとして受け取る。 +4. `'商品ID: prod_abc, 価格: ¥3500, 在庫数: 12個'` と出力する。 + +```dart:practice9_2.dart +// ここに関数を定義してください + +void main() async { + // ここで動作確認を行ってください +} +``` + +```dart-exec:practice9_2.dart +``` diff --git a/public/docs/dart/index.yml b/public/docs/dart/index.yml new file mode 100644 index 00000000..80867126 --- /dev/null +++ b/public/docs/dart/index.yml @@ -0,0 +1,42 @@ +name: Dart +description: Googleによって開発された、Flutterの基盤となるUI最適化・マルチプラットフォーム言語 +pages: +- slug: 0-intro + name: Dartへようこそ + title: Dartへようこそ +- slug: 1-basics + name: 基本構文 + title: 基本構文と組み込み型 +- slug: 2-null-safety + name: Null Safety + title: Null Safetyの完全理解 +- slug: 3-functions + name: 関数とクロージャ + title: 関数とクロージャ +- slug: 4-collections-control + name: コレクションと制御構文 + title: コレクションと制御構文 +- slug: 5-records-patterns + name: レコードとパターンマッチ + title: レコードとパターンマッチング +- slug: 6-classes + name: クラスとコンストラクタ + title: クラスとコンストラクタ +- slug: 7-class-extension + name: クラスの拡張と高度な設計 + title: クラスの拡張と高度な設計 +- slug: 8-class-modifiers + name: クラス修飾子とモデリング + title: クラス修飾子による厳密なモデリング +- slug: 9-async-future + name: '非同期処理: Future' + title: 非同期処理の基礎 (Futureとasync/await) +- slug: 10-async-stream + name: '非同期処理: Stream' + title: 非同期処理の応用 (Stream) +- slug: 11-error-handling + name: エラーハンドリング + title: エラーハンドリングとResultパターン +- slug: 12-concurrency-isolate + name: 並行処理とIsolate + title: 並行処理とIsolate