スクリプトオブジェクトのプロパティにラベル文字列でアクセスする、よりスマートな方法.

【実行環境】スクリプトエディタ または アプリケーションバンドル


まず前提としてある考えなのですが、レコードの項目に文字列でアクセスするために以下のコードを書きました.レコードを一度 NSDictionary オブジェクトへ変換してしまえば valueForKey: メソッドを使用して値を取り出すことができます.AppleScript の命令だけでそれを実現しようと思うと手間がかかるので、手っ取り早くそれを利用しました.


recordValueForKey01.scpt

use AppleScript version "2.5"
use framework "Foundation"
use scripting additions
set aRecord to {a:"Alphabet", b:"Balance"}
set aDict to current application's NSDictionary's dictionaryWithDictionary:aRecord
log (aDict's valueForKey:"a") as string # log (*Alphabet*)
log (aDict's valueForKey:"b") as string # log (*Balance*)


そしてこちらが本題です.上記の内容を踏まえ、同様のことをスクリプトオブジェクトに対してもできないかといろいろ試したのですが、そもそもスクリプトオブジェクトを Objective-C で扱えるオブジェクトへ変換する方法がわからず断念しました.それができれば一番良いのですが…….

そこで少し方向性を変えて「script 内に valueForKey: メソッドを実装してそれを呼び出す」コードを以下のように書きました.


scriptObjectValueForKey03.scpt

use AppleScript version "2.5" -- OS X 10.11 or later
use framework "Foundation"
use scripting additions
property NSString : a reference to current application's NSString
script aScriptObject
	property a : "Alphabet"
	property b : "Balance"
	on valueForKey:argKey
		set nKey to NSString's stringWithString:argKey
		if nKey's isEqualToString:"a" then return me's a
		if nKey's isEqualToString:"b" then return me's b
	end valueForKey:
	on setValue:argValue forKey:argKey
		set nKey to NSString's stringWithString:argKey
		if nKey's isEqualToString:"a" then set me's a to argValue
		if nKey's isEqualToString:"b" then set me's b to argValue
	end setValue:forKey:
end script
log (aScriptObject's valueForKey:"a") as string
log (aScriptObject's valueForKey:"b") as string
aScriptObject's setValue:"AppleScript" forKey:"a"
log (aScriptObject's valueForKey:"a") as string
log (aScriptObject's valueForKey:"b") as string


たしかにこの方法でも結果的に「ラベル文字列でアクセスする」ことはできたのですが、property 命令の項目が増えれば増えるだけvalueForKey: メソッド内のコードも増えますし、これは少し別件ですが、セッターメソッド(今回の場合 setValue:forKey:)も用意しようと思うと更に倍近くコードの量が増えてしまいます.けれどこれはあまりスマートな方法でないのでは? と思うのです.事実、recordValueForKey01.scpt の件で言うと、レコードの要素が増えたとしても、そのレコードの要素に対する記述が増えるだけで(例えば {a:"Alphabet", b:"Balance", c:"Copland"} )、それ以上の他のコストはかからないわけですし.

どうにかして Objective-C のメソッドを駆使して問題を解決することはできないでしょうか.どうかお知恵をお貸しください.

Mac mini, macOS 10.14

投稿日 2021/06/21 22:09

返信
スレッドに付いたマーク ランキングトップの返信

投稿日 2021/07/08 22:21

> ただ、このmeをスクリプトオブジェクトに替えて指定すると以下の部分でエラーとなるようです


確かに、スクリプトオブジェクトだとダメですね。

結局のところ、FoundationのCollectionsをインスタンス化するときに、スクリプトオブジェクトがObjective-Cオブジェクトに変換されるようです。Collectionの中ではNSSetが一番軽いという話なので、NSSetで書いてみました。

(NSValueのvalueWithNonretainedObjectで取り込んでみましたが変換されず


ObjectTest.scpt

use framework "Foundation"
use scripting additions

script myScript
	property a : "a"
	property b : 100
	script c
		property a : "c.a"
		property b : true
	end script
end script

set myCless to load script (do shell script "echo $HOME/Desktop/ObjectClass.scpt")
set myObject to myCless's objectWithScript:myScript
log (myObject's className()) (*BAGenericObject*)
log (myObject's classNameForKeyPath:"a") (*NSTaggedPointerString*)
log (myObject's classNameForKeyPath:"b") (*__NSCFNumber*)
log (myObject's classNameForKeyPath:"c") (*BAGenericObject*)
log (myObject's valueForKeyPath:"a") (*a*)
log (myObject's valueForKeyPath:"b") (*100*)
log (myObject's valueForKeyPath:"c") (*missing value*)
log (myObject's valueForKeyPath:"c.a") (*c.a*)
myObject's setValue:"c.A" forKeyPath:"c.a"
log (myObject's valueForKeyPath:"c.a") (*c.A*)
myObject's release()


ObjectClass.scpt

use framework "Foundation"
use scripting additions

--property NSArray : a reference to current application's NSArray
property NSSet : a reference to current application's NSSet

script myInstance
	property myObject : null
	on release()
		set myObject to null
		return
	end release
	on className()
		return myObject's className() as text
	end className
	on classNameForKeyPath:myKeyPath
		return (myObject's valueForKeyPath:myKeyPath)'s className() as text
	end classNameForKeyPath:
	on setValue:myValue forKeyPath:myKeyPath
		if (me's classNameForKeyPath:myKeyPath) is not equal to "BAGenericObject" then
			myObject's setValue:myValue forKeyPath:myKeyPath
		else
			tell me to error "invalid KeyPath"
		end if
	end setValue:forKeyPath:
	on valueForKeyPath:myKeyPath
		if (me's classNameForKeyPath:myKeyPath) is not equal to "BAGenericObject" then
			return (myObject's valueForKeyPath:myKeyPath) as text
		else
			return missing value
		end if
	end valueForKeyPath:
end script

on objectWithScript:myScript
	--set myInstance's myObject to (NSArray's arrayWithObject:myScript)'s firstObject()
	set myInstance's myObject to (NSSet's setWithObject:myScript)'s anyObject()
	if myInstance's className() is not equal to "BAGenericObject" then
		tell me to error "invalid ScriptObject: '" & myInstance's className() & "'"
	end if
	return myInstance
end objectWithScript:


BAGenericObjectがscriptのクラスで、BAGenericObjectNoDeleteOSAIDがスクリプトファイルのようです。

4000個のpropertyを書いて、NSArrayとNSSetとで実行速度を測ってみましたが、差はありませんでした。


返信: 8
スレッドに付いたマーク ランキングトップの返信

2021/07/08 22:21 light289 への返信

> ただ、このmeをスクリプトオブジェクトに替えて指定すると以下の部分でエラーとなるようです


確かに、スクリプトオブジェクトだとダメですね。

結局のところ、FoundationのCollectionsをインスタンス化するときに、スクリプトオブジェクトがObjective-Cオブジェクトに変換されるようです。Collectionの中ではNSSetが一番軽いという話なので、NSSetで書いてみました。

(NSValueのvalueWithNonretainedObjectで取り込んでみましたが変換されず


ObjectTest.scpt

use framework "Foundation"
use scripting additions

script myScript
	property a : "a"
	property b : 100
	script c
		property a : "c.a"
		property b : true
	end script
end script

set myCless to load script (do shell script "echo $HOME/Desktop/ObjectClass.scpt")
set myObject to myCless's objectWithScript:myScript
log (myObject's className()) (*BAGenericObject*)
log (myObject's classNameForKeyPath:"a") (*NSTaggedPointerString*)
log (myObject's classNameForKeyPath:"b") (*__NSCFNumber*)
log (myObject's classNameForKeyPath:"c") (*BAGenericObject*)
log (myObject's valueForKeyPath:"a") (*a*)
log (myObject's valueForKeyPath:"b") (*100*)
log (myObject's valueForKeyPath:"c") (*missing value*)
log (myObject's valueForKeyPath:"c.a") (*c.a*)
myObject's setValue:"c.A" forKeyPath:"c.a"
log (myObject's valueForKeyPath:"c.a") (*c.A*)
myObject's release()


ObjectClass.scpt

use framework "Foundation"
use scripting additions

--property NSArray : a reference to current application's NSArray
property NSSet : a reference to current application's NSSet

script myInstance
	property myObject : null
	on release()
		set myObject to null
		return
	end release
	on className()
		return myObject's className() as text
	end className
	on classNameForKeyPath:myKeyPath
		return (myObject's valueForKeyPath:myKeyPath)'s className() as text
	end classNameForKeyPath:
	on setValue:myValue forKeyPath:myKeyPath
		if (me's classNameForKeyPath:myKeyPath) is not equal to "BAGenericObject" then
			myObject's setValue:myValue forKeyPath:myKeyPath
		else
			tell me to error "invalid KeyPath"
		end if
	end setValue:forKeyPath:
	on valueForKeyPath:myKeyPath
		if (me's classNameForKeyPath:myKeyPath) is not equal to "BAGenericObject" then
			return (myObject's valueForKeyPath:myKeyPath) as text
		else
			return missing value
		end if
	end valueForKeyPath:
end script

on objectWithScript:myScript
	--set myInstance's myObject to (NSArray's arrayWithObject:myScript)'s firstObject()
	set myInstance's myObject to (NSSet's setWithObject:myScript)'s anyObject()
	if myInstance's className() is not equal to "BAGenericObject" then
		tell me to error "invalid ScriptObject: '" & myInstance's className() & "'"
	end if
	return myInstance
end objectWithScript:


BAGenericObjectがscriptのクラスで、BAGenericObjectNoDeleteOSAIDがスクリプトファイルのようです。

4000個のpropertyを書いて、NSArrayとNSSetとで実行速度を測ってみましたが、差はありませんでした。


2021/06/23 00:53 light289 への返信

AppleScriptはスクリプト言語ですので、クラス(今回の場合はRecord Objects)の拡張はできず擬似的なものになってしまいます。スクリプト言語としての簡易性を捨ててオブジェクト指向のようなものを目指すのであれば、素直にAppleScriptは止めて、SwiftやObjective-Cに移行するのが適切だと考えます。簡易性を重んじるのであれば、出来合いのもの(=今回の場合はNSMutableDictionary)を使うのが良いのではないでしょうか。複雑になる程処理速度は低下します。


上記の理由と、起動に時間が掛かるAppleScriptは避ける傾向にあるのですが(大抵Swiftで書いています)、下記のようなことをしている人もいます。参考までに。


AppleScriptでクラスベースオブジェクト指向プログラミング

http://www.script-factory.net/monologue/ClassWithScriptObject/index.xhtml

2021/06/21 23:10 light289 への返信

NSDictionaryは変更不可ですが、NSMutableDictionary​なら変更可能です。


NSMutableDictionary

https://developer.apple.com/documentation/foundation/nsmutabledictionary


use framework "Foundation"
use scripting additions
set aDict to current application's NSMutableDictionary's dictionary

aDict's setObject:"Alphabet" forKey:"a"
aDict's setObject:"Balance" forKey:"b"

display dialog (aDict's objectForKey:"a") as string #Alphabet
display dialog (aDict's objectForKey:"b") as string #Balance

aDict's removeObjectForKey:"a"

display dialog (aDict's objectForKey:"a") as string #missing value
display dialog (aDict's objectForKey:"b") as string #Balance

2021/07/05 00:33 light289 への返信

自己レス、および自己解決となってしまいましたが、どうにか解決できました.


accessPropertiesScriptObjectWithLabel-forKey.scpt

use framework "Foundation"
property NSArray : a reference to current application's NSArray

script MyScriptObject
	property c : "Candidate"
	property k : "Kick it, wake up."
end script
property a : "Alphabet"
property b : "Balance"
property s : missing value

set s to "Spotlight"

#	スクリプトオブジェクト MyScriptObject
set theGenericObject to (NSArray's arrayWithArray:{MyScriptObject})'s firstObject()
log theGenericObject's className() as string (*BAGenericObject*)

set cProp to (theGenericObject's valueForKey:"c") # MyScriptObject の c を読み込む.
theGenericObject's setValue:"Kingdom" forKey:"k" # MyScriptObject の k  を書き換える.
log cProp's className() as string (*NSTaggedPointerString*)
log cProp as string (*Candidate*)
log MyScriptObject's k's className() as string (*NSTaggedPointerString*)
log MyScriptObject's k as string (*Kingdom*)

#	トップレベル me
set theGenericObject to (NSArray's arrayWithArray:{me})'s firstObject()
set aProp to (theGenericObject's valueForKey:"a") # トップレベル の a を読み込む.
theGenericObject's setValue:"Broadway" forKey:"b" # トップレベル の b  を書き換える.
log aProp's className() as string (*NSTaggedPointerString*)
log aProp as string (*Alphabet*)
log b's className() as string (*NSTaggedPointerString*)
log b as string (*Broadway*)
log s (*Spotlight*)

theGenericObject's setValue:MyScriptObject forKey:"s" -- トップレベル の s を書き換える.
log s's className() as string (*BAGenericObject*)
log class of (s as script) (*script*)
log name of (s as script) (*MyScriptObject*)


accessPropertiesScriptObjectWithLabel-forKeyPath.scpt

use framework "Foundation"
property NSArray : a reference to current application's NSArray

script MyScriptObject
	property c : "Candidate"
	property k : "Kick it, wake up."
end script
property a : "Alphabet"
property b : "Balance"
property s : missing value

set s to "Spotlight"

#	トップレベル me
set theGenericObject to (NSArray's arrayWithArray:(me as list))'s firstObject()
log theGenericObject's className() as string (*BAGenericObjectNoDeleteOSAID*)
log theGenericObject's superclass()'s className() as string (*BAGenericObject*)

set cProp to (theGenericObject's valueForKeyPath:"MyScriptObject.c") -- MyScriptObject の c を読み込む.
theGenericObject's setValue:"Kingdom" forKeyPath:"MyScriptObject.k" -- MyScriptObject の k を書き換える.
log cProp's className() as string (*NSTaggedPointerString*)
log cProp as string (*Candidate*)
log MyScriptObject's k's className() as string (*NSTaggedPointerString*)
log MyScriptObject's k as string (*Kingdom*)

set aProp to (theGenericObject's valueForKeyPath:"a") -- トップレベル の a を読み込む.
theGenericObject's setValue:"Broadway" forKeyPath:"b" -- トップレベル の b を書き換える.
log aProp's className() as string (*NSTaggedPointerString*)
log aProp as string (*Alphabet*)
log b's className() as string (*NSTaggedPointerString*)
log b as string (*Broadway*)
log s (*Spotlight*)

theGenericObject's setValue:MyScriptObject forKeyPath:"s" -- トップレベル の s を書き換える.
log s's className() as string (*BAGenericObject*)
log class of (s as script) (*script*)
log name of (s as script) (*MyScriptObject*)


> accessPropertiesScriptObjectWithLabel-forKey.scpt

「valueForKey:」および「setValue:forKey:」を使用してアクセスする.ネストされたスクリプトオブジェクトの扱いには不向き.


> accessPropertiesScriptObjectWithLabel-forKeyPath.scpt

「valueForKeyPath:」および「setValue:forKeyPath:」を使用してアクセスする.個人的にはいくつかのネストされたスクリプトオブジェクトをよく扱うので、ドットシンタックスが利用できるこちらの記述のほうがしっくりきます.


どちらのサンプルスクリプトもログを含めているので少し長く見えますが、最も重要なコードはAppleScript型をObjective-Cオブジェクトへと変換する下記のたった1行分であり、わかってしまえばなんということもない、とても簡単なものでした.いや、まあ、試行錯誤の中でいろいろ学ぶこともあったので無駄だったとは思いませんが.

set theGenericObject to (NSArray's arrayWithArray:(me as list))'s firstObject()

ただしこの1行は、おそらく他にもっと良い記述方法があると思っています.少なくとも、あまり格好が良いとは思えません.要するにスクリプトオブジェクトを何らかのカタチで「NSObjectを継承したObjective-Cオブジェクト」に変換できれば良いのですが(つまりvalueForKey:メソッドなどを使いたい)、今のところ、これ以外に思いつかなかったもので.

2021/06/22 05:04 hohokihai への返信

レコードへのアクセスについての自身の記述は「それと同等の手法」を提示するための例であり、実際に模索しているのは「スクリプトオブジェクトの内部で定義されたプロパティへの valueForKey: メソッドなどを用いた指定文字列によるアクセス方法」です.

せっかくサンプルスクリプトまで用意していただいたのに恐縮です…….

2021/07/08 08:06 hohokihai への返信

set theGenericObject to me

ただ、この me を スクリプトオブジェクトに替えて指定すると以下の部分でエラーとなるようです.

log theGenericObject's className() as string

それでも valueForKeyPath: の実行前に変数などを用意してそこから実行するよりも遙かに簡便だと思うので重宝しそうです.

2021/06/25 04:16 light289 への返信

> 実際に模索しているのは「スクリプトオブジェクトの内部で定義されたプロパティへのvalueForKey:メソッドなどを用いた指定文字列によるアクセス方法」です


実のところ何を目指しているのかよく分かっていないのですが、例えば、NSDictionaryの拡張を行いたいのであれば、次のように書けます。


use AppleScript version "2.5"
use framework "Foundation"
use scripting additions
script aScriptObject
	property aDictionary : null
	on init()
		set aDictionary to current application's NSMutableDictionary's dictionary
		return
	end init
	on free()
		set aDictionary to null
		return
	end free
	on setObjects:argObjects forKeys:argKeys
		if aDictionary is not null then
			aDictionary's setObjects:argObjects forKeys:argKeys
		end if
	end setObjects:forKeys:
	on setObject:argObject forKey:argKey
		if aDictionary is not null then
			aDictionary's setObject:argObject forKey:argKey
		end if
	end setObject:forKey:
	on objectForKey:argKey
		if aDictionary is not null then
			return (aDictionary's objectForKey:argKey) as string
		end if
	end objectForKey:
end script

aScriptObject's init()
aScriptObject's setObjects:{"Alphabet", "Balance"} forKeys:{"a", "b"}
aScriptObject's setObject:"Color" forKey:"c"
log ("a = " & (aScriptObject's objectForKey:"a"))
log ("b = " & (aScriptObject's objectForKey:"b"))
log ("c = " & (aScriptObject's objectForKey:"c"))
aScriptObject's free()



下記エラー対策の為、init()とfree()を用意しています。

osascript: couldn't save changes to script <filename>: error -1763.


(valueForKeyはKVC由来のメソッドなので、NSDictionaryなのであればobjectForKeyにした方がよいかと思います。

2021/07/06 00:33 light289 への返信

おお、valueForKeyPathがちゃんと動いてますね。meを直接入れてもいけるみたいですよ。


use framework "Foundation"

script MyScriptObject
	property c : "Candidate"
	property k : "Kick it, wake up."
end script
property a : "Alphabet"
property b : "Balance"

set theGenericObject to me
log theGenericObject's className() as string (*BAGenericObjectNoDeleteOSAID*)
log theGenericObject's superclass()'s className() as string (*BAGenericObject*)
log theGenericObject's superclass()'s superclass()'s className() as string (*BAObjectProto*)
log theGenericObject's superclass()'s superclass()'s superclass()'s className() as string (*NSObject*)

log (theGenericObject's valueForKeyPath:"MyScriptObject.k") as string (*Kick it, wake up.*)
theGenericObject's setValue:"Kingdom" forKeyPath:"MyScriptObject.k"
log (theGenericObject's valueForKeyPath:"MyScriptObject.k") as string (*Kingdom*)

このスレッドはシステム、またはAppleコミュニティチームによってロックされました。 問題解決の参考になる情報であれば、どの投稿にでも投票いただけます。またコミュニティで他の回答を検索することもできます。

スクリプトオブジェクトのプロパティにラベル文字列でアクセスする、よりスマートな方法.

Apple サポートコミュニティへようこそ
Apple ユーザ同士でお使いの製品について助け合うフォーラムです。Apple Account を使ってご参加ください。