力あるインフラエンジニアを目指して

IT初心者が人前にインフラエンジニアですと言っても恥ずかしくないようになる日を目指します。

undefの値同士をつなげると空の文字列になる?

「undefの値同士をつなげると空白の文字列になる?」

と、お仕事のときperlスクリプト書いてるときに思いました。

この記事のタイトル考えたのはperlスクリプト書いてて嵌った後になんとなく光が見え始めた時期でまだ解決できてなかったので勘違いしてるように見えますが、なんとなく違いがわかったので折角の機会てことでまとめてみました。 undefの値と空文字って一体何が違うんだろうと思いました。両方とも空っぽな中身に見えるんだけど?と思いました。 ということで、スクリプト作りつつ両者の違いを下がっていこうということで作ってるスクリプトに両者の違いが理解できるような出力加えていってみました。

スクリプトの概要

ちょっとお仕事で書いてたスクリプトについて簡単に。 AというDBにあるテーブルBから値をいくつか取ってきてその中に入ってる文字列を結合させてCというDBにあるテーブルDに入れるというスクリプトでした。 仮の値使って表すと

DB A テーブルB

datetime alarm email call
2018-01-13 13:14:35 ping NG hoge@hoge.comまでメールを送信してください
2018-01-13 13:15:59 Link Down 090xxxxxxxxまで電話してください
2018-01-13 13:17:25

perlスクリプトを実行した結果

DB C テーブルD

datetime text
2018-01-13 13:14:35 ping NG hoge@hoge.comまでメールを送信してください
2018-01-13 13:15:59 Link Down 090xxxxxxxxまで電話してください
2018-01-13 13:17:25

という風にしようとしたらDB C テーブル DのtextカラムでNULLの値を入れられないように設定してたのでエラーを吐きました。そのときにDB A テーブル Bのalarm, email, callの値が入れられるであろう変数がundefとなっているけど、それらの値を結合して作ったtextの中身は出力すると''と空文字になっていると気づきました。

ちなみに、textの中身がないものはテーブルDにも要らないので普通に空文字でない場合のみテーブルDへ値を挿入するように処理を変更したので動作としてはその後問題ありませんでした。

テストコードによる検証

"" ソースコード 上で説明している例を元に検証できるようなテストコード作って見ました。下のような感じで。
※下のテストコードは上の説明と異なり、同じDB上にあるテーブルでやりとりするものです。  テーブルBをsrcというテーブル名、テーブルDをdstというテーブル名にしてます。 ※※DBはスクリプトを実施するコンピュータ上に置いたのでlocalhost指定してます。

#!/usr/bin/perl

use strict;
use warnings;
use DBI;
use Data::Dumper;

# DB接続用
our $DB_NAME = "test";
our $DB_USER = "root";
our $DB_PASS = "";
our $DB_HOST = "localhost";
our $DB_PORT = "3306";

# mysqlサーバへ接続
my $dbh = DBI->connect("dbi:mysql:dbname=$DB_NAME;host=$DB_HOST;port=$DB_PORT","$DB_USER","$DB_PASS") or die "$!\n Error: failed to connect to DB.\n";

print "====================DB処理開始====================\n";
# テーブルsrcからSELECTするSQL文
my $sql1 = "SELECT * FROM src;";
my $sth1 = $dbh->prepare($sql1);
# テーブルsrcからSELECTするSQL実行
$sth1->execute;
# テーブルdstへINSERTするSQL文
my $sql2 = "INSERT INTO dst (datetime, text) VALUES (?,?);";

while (my $hash_ref = $sth1->fetchrow_hashref) {
  # textにhashの値を結合させる
  $hash_ref->{text} = $hash_ref->{alarm}. $hash_ref->{email}. $hash_ref->{call};
  print "結合結果の確認:$hash_ref->{text}\n";
  
  if ($hash_ref->{text} eq '') {
    print "\"text\"は空文字列です\n";
  }
  else {
    print "\"text\"は空文字列ではありません\n";
  }
  
  if (defined($hash_ref->{text})) {
    print "\"text\"は定義されています\n";
  }
  else {
    print "\"text\"は未定義です\n";
  }
  
  if ($hash_ref->{alarm} eq '') {
    print "\"alarm\"は空文字列です\n";
  }
  else {
    print "\"alarm\"は空文字列ではありません\n";
  }
  if (defined($hash_ref->{alarm})) {
    print "\"alarm\"は定義されています\n";
  }
  else {
    print "\"alarm\"は未定義です\n";
  }

  # $hash_refに格納されている値を出力
  print Dumper $hash_ref;
      
  my $sth2 = $dbh->prepare($sql2);
  # INSERTのSQL文を実行
  $sth2->execute($hash_ref->{"time"},
                 $hash_ref->{"text"});
  $sth2->finish;
}

# SQL文を開放
$sth1->finish;

# データベースから切断   
$dbh->disconnect;

print "====================DB処理完了====================\n"

出力結果

 $ ./undeftest.pl 
====================DB処理開始====================
Use of uninitialized value in concatenation (.) or string at ./undeftest.pl line 29.
結合結果の確認:ping NGhoge@hoge.com
"text"は空文字列ではありません
"text"は定義されています
"alarm"は空文字列ではありません
"alarm"は定義されています
$VAR1 = {
          'alarm' => 'ping NG',
          'call' => undef,
          'time' => '2018-01-13 13:14:35',
          'text' => 'ping NGhoge@hoge.com',
          'email' => 'hoge@hoge.com'
        };
Use of uninitialized value in concatenation (.) or string at ./undeftest.pl line 29.
結合結果の確認:Link Down080xxxxxxxx
"text"は空文字列ではありません
"text"は定義されています
"alarm"は空文字列ではありません
"alarm"は定義されています
$VAR1 = {
          'email' => undef,
          'text' => 'Link Down080xxxxxxxx',
          'time' => '2018-01-13 13:15:59',
          'alarm' => 'Link Down',
          'call' => '080xxxxxxxx'
        };
Use of uninitialized value in concatenation (.) or string at ./undeftest.pl line 29.
Use of uninitialized value in concatenation (.) or string at ./undeftest.pl line 29.
Use of uninitialized value in concatenation (.) or string at ./undeftest.pl line 29.
結合結果の確認:
"text"は空文字列です
"text"は定義されています
Use of uninitialized value in string eq at ./undeftest.pl line 46.
"alarm"は空文字列です
"alarm"は未定義です
$VAR1 = {
          'call' => undef,
          'alarm' => undef,
          'time' => '2018-01-13 13:17:25',
          'text' => '',
          'email' => undef
        };
====================DB処理完了====================

最後の出力結果から、alarmは未定義だが、textは定義済とわかります。しかし、お互いに空文字列です。textを生成するにあたり用いるemail, callも未定義であるから、 これらの3つの未定義の値を結合したtextの値も未定義になるのでは?と当時は思っていました(よく考えればそんなことはないだろうって話ですが)。 Dumperの出力結果見ても表記が'alarm' => undef,'text' => '',と異なってますね。私は最初このDumperの値だけを見てたのもあって混乱しました笑 「あれ?alarmはundefだけどtextだけ""になってるのなんで?なんで定義されてることになってるの?」と。

まとめ

今回の検証やってみてなんとなくわかったこととしては

  • undefと空文字列は違うということ。
    • undefはその文字通りそもそも値が何も定義されていない、そのため文字列として出力すると空文字を返す。
    • 空文字列は空文字列としてその変数に格納されているので空文字としてdefinedされているということ。

とは言えネットで検索するとすぐに「perl オワコン」「perl 時代遅れ」と単語が並ぶ中一番始めに扱った言語がperlとは… 直属の上司にもperl辞めてpythonに変えようとよく言われてるので変えないといけないですね。 現在進行形でperlも使わないといけないのでperlの勉強しつつpythonを勉強して、新しいものはpythonで書いて古いものはpython形式に書き換えていくことになりそうです。

おまけ

$hash_ref{text}が空じゃないときだけテーブルにINSERTするということで

  unless($hash_ref->{text} eq ''){
    $sth2->execute($hash_ref->{"time"},
                   $hash_ref->{"text"});
    $sth2->finish;
  }

的なことをお仕事で作ったスクリプトでは実施。と言っても条件式微妙に違うんですけどね。。