関数化するときにハマった時に注目するたった一つのこと

本投稿は TECOTEC Advent Calendar 2025 の4日目の記事です。

DX本部・システム開発第一事業部のIです。 普段はエンジニアとして、TypeScriptでシステムを開発したり、PythonでGPUを使った最適化をしたりしています。

本記事はタイトルの通りで、リファクタリングの一環で関数化・メソッド化した際に、思わぬ結果に出くわしたときに役立つ注目点について、駆け足で解説していきたいと思います。

目次

はじめに

ある時、リファクタリングの一環として処理を関数化して実行すると、以下のような現象に出くわしてしまい、原因がわからずハマってしまう経験はおありでしょうか。

  • 関数化前と明らかに結果が違うけど、どこで変わったのか分からない
  • 関数内でエラーが出たが何が原因なのかわからない

実は、これらの現象は、たった一つの要因を見落としたことが切っ掛けで発生することがほとんどです。

その要因とは、「実装している言語での、関数の引数(戻り値)の特性」です。

こう述べると、「言語ごとに特性を把握!? なんかめんどくさそう…」とモチベーションが下がりそうですが、実は特性は大きく分けて3種類程度です。 また、すべての言語の特性を把握する必要はありません。プロジェクトにふさわしい言語の特性をつかめておくだけで十分です。

※本記事は駆け足で説明していきますので、細かい部分はご自身で調査していただけると幸いです。

引数の3種類の特性

早速ですが、引数に関する3種類の特性とは、すべて引数の渡し方に関するもので、それぞれ「値渡し」「参照渡し」「共有渡し」と呼びます。

これらは、それぞれ以下のような振る舞いをしています。

値渡し

値渡し」は、実引数の値をコピーして、仮引数にする渡し方です。

機械的には、渡したい引数をスタックにコピー しています。

値渡しでは、実引数と仮引数は別々のものと考えます。そのため、仮引数に代入をしても実引数には影響ありません。

実引数の値は変わらないため、この振る舞いを「不変」を意味する「イミュータブル」と呼ぶこともあります。

しかし、実引数の構成に参照が存在し、参照先を更新したときは実引数に影響が出ます。

値渡しを採用している言語

標準で採用しているものとして、CC++C#*1RustJavaJavaScriptPHPVisual Basic .NET などがあります。

Visual Basic(6まで)VBA では、ByVal といった、特別な演算子を付けることで、値渡しを明示する言語もあります。

参照渡し

参照渡し」は、実引数の参照をそのまま仮引数の参照にする渡し方です。

機械的には、渡したい引数の参照をスタックにコピー しています。

参照渡しでは、実引数と仮引数は同じもの(同じ名札を使いまわすようなイメージ)と考えます。そのため、仮引数に代入を行うと、実引数も代入したものになります。

実引数の値が変わるため、この振る舞いを「可変」を意味する「ミュータブル」と呼ぶこともあります。

もちろん、仮引数を構成する値や参照を更新したときも実引数に影響が出ます。

参照渡しを採用している言語

標準で採用しているものとして、Visual Basic(6まで)VBAFORTRAN77*2 などがあります。

C++*3C# では &Rust では &&mutVisual Basic .NETでは ByRef といった、特別な演算子を付けることで、参照渡しを明示する言語もあります。

共有渡し

共有渡し」は、実引数の参照をコピーして、仮引数の参照にする渡し方です。

機械的には、「参照渡し」と同じく、渡したい引数の参照をスタックにコピー しています。

共有渡しでは、実引数と仮引数は別々のものと考える(名札を付け替えるようなイメージ)ので、仮引数に代入をしても実引数には影響ありません。

実引数の値は変わらないため、値渡しと同様に「不変」を意味する「イミュータブル」と呼ぶこともあります。

値渡しと同様に、実引数の構成に参照が存在し、参照先を更新したときは実引数に影響が出ます。

共有渡しを採用している言語

標準で採用しているものとして、C#PythonRubyPHPJavaScript などがあります。

Rust では Rc<T>Arc<T> といった、特別な演算子を付けることで、共有渡しを明示する言語もあります。

どの言語でも、どのデータ構造でも、上記3種類のどれかに集約されますので、これらの3種類を理解しておけば、問題なく実装できます*4

それでも勘違いしてしまうとき

とはいえ、上記3種類をキチンと把握できたとしても、それだけでは対応できないときがあります。 それは、引数として渡すものによって渡し方が変わるときがあるからです。

先ほど説明したとき、複数の特性(しかも、それぞれ特別な演算子が必要ない)言語があるのに気づかれたと思います。 とくに、「値渡し」と「共有渡し」を両方持っている言語がほとんどです。

これらの違いは、「言語が持つ型の構成の違い」によるもので、構成はそれぞれ、大きく分けて「プリミティブ型」と「オブジェクト型」という2種類に分かれます*5

プリミティブ型は、データそのものを「プリミティブ」として保持しています。 変数として扱うときは、プリミティブをそのまま保持しています。

オブジェクト型は、データや振る舞いをまとめて「オブジェクト」として保持しています。 変数として扱うときは、オブジェクトの参照をそのまま保持しています。

お気づきかと思いますが、つまり、それぞれの特徴生かすために使い分ける必要があるわけです。

  • プリミティブ型を引数として渡すときは値渡し
  • オブジェクト型を引数として渡すときは共有渡し

ただし、どの型がどう振る舞うかについては、各言語の仕様書をご参照ください*6

戻り値の返し方

ここまでで、引数の渡し方を述べてきましたが、実は戻り値の返し方も「値渡し」「参照渡し」「共有渡し」のどれかになります。

もちろん、プリミティブ型かオブジェクト型、特別な演算子が付くかどうかでも変わります*7

そのため、引数の渡し方と戻り値の返し方を一緒にチェックできるようにしておくと捗ります。

問題解消への道筋

ここまで把握できるようにしておくと、引数と呼び元の変数にデバッグプリントを挟んで時系列で内容の変化を確認することで、現象を把握できやすくなったり、問題の原因を突き止めやすくなります。

また、これから実装するときには、言語の特性を把握・確認したり、できるようにしたりすることでバグを埋め込むリスクを減らせます。

あと、実装するときに次の点を心がけると、さらにバグを減らせると考えています。

  • 関数を呼び出して値を更新したい場合、引数のミュータブル型・イミュータブル型を当てにせず、最終的な結果を戻り値として返すようにする
  • 戻り値のスコープに注意する(特にCやC++の場合)

まとめ

最後に、引数(戻り値)の渡し方について、一通り解説いたしました。改めて、それぞれの特徴を表にしました。

引数の渡し方に関する一覧表

基本的に、この表を実装やAIに訊いたときにチェックすることで思わぬバグを事前に回避できると思います。

また、これは当方の経験によるものですが、案件が変わったときに実装言語が変わったことによって、前の案件の言語仕様を引きずってしまうことも考えられます。 その際も、引数や戻り値だけではなく、型の仕様のチェックも心がけることは重要と考えています。

更に、昨今、AIにコードを実装してもらう機会も増えるかと存じますが、言語仕様の理解を怠っていたがゆえにハルシネーションに振り回されることがあるので、これからも、仕様の確認などといった技術の研鑽を欠かさないで行きたいものですね。

それでは、丁度時間となりましたので、この辺で失礼いたします。ありがとうございました。

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。
採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。
ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp

*1:特に、struct(構造型)はクラスに似ていますが値型で、代入するとコピーが発生します

*2:この言語の参照渡しは悪い意味で有名で、リテラルを引数として渡すと、関数内で代入した値になってしまうというとんでもないバグがあります

*3:C++では、クラスのインスタンスも「値」として扱います

*4:ただし、Rustでは、値ごとに「所有権」が存在し、渡し方によって所有権の扱いが変わるようになっています

*5:実はJavaScriptもプリミティブ型とオブジェクト型が共存しています

*6:何がプリミティブ型かオブジェクト型かは言語の差はあまり変わりませんが、PHPの配列は特殊で、オブジェクト型ながら値渡し(シャローコピー)という珍しい振る舞いを行います

*7:それゆえ、C++では「ダングリングポインタ」というバグの温床が存在します